diff --git a/.github/actions/await-http-resource/action.yml b/.github/actions/await-http-resource/action.yml new file mode 100644 index 000000000..ba177fb75 --- /dev/null +++ b/.github/actions/await-http-resource/action.yml @@ -0,0 +1,20 @@ +name: Await HTTP Resource +description: 'Waits for an HTTP resource to be available (a HEAD request succeeds)' +inputs: + url: + description: 'URL of the resource to await' + required: true +runs: + using: composite + steps: + - name: Await HTTP resource + shell: bash + run: | + url=${{ inputs.url }} + echo "Waiting for $url" + until curl --fail --head --silent ${{ inputs.url }} > /dev/null + do + echo "." + sleep 60 + done + echo "$url is available" diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 000000000..70b051310 --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,66 @@ +name: Build +description: 'Builds the project, optionally publishing it to a local deployment repository' +inputs: + develocity-access-key: + description: 'Access key for authentication with ge.spring.io' + required: false + gradle-cache-read-only: + description: 'Whether Gradle''s cache should be read only' + required: false + default: 'true' + java-distribution: + description: 'Java distribution to use' + required: false + default: 'liberica' + java-early-access: + description: 'Whether the Java version is in early access' + required: false + default: 'false' + java-toolchain: + description: 'Whether a Java toolchain should be used' + required: false + default: 'false' + java-version: + description: 'Java version to compile and test with' + required: false + default: '17' + publish: + description: 'Whether to publish artifacts ready for deployment to Artifactory' + required: false + default: 'false' +outputs: + build-scan-url: + description: 'URL, if any, of the build scan produced by the build' + value: ${{ (inputs.publish == 'true' && steps.publish.outputs.build-scan-url) || steps.build.outputs.build-scan-url }} + version: + description: 'Version that was built' + value: ${{ steps.read-version.outputs.version }} +runs: + using: composite + steps: + - name: Prepare Gradle Build + uses: ./.github/actions/prepare-gradle-build + with: + cache-read-only: ${{ inputs.gradle-cache-read-only }} + develocity-access-key: ${{ inputs.develocity-access-key }} + java-distribution: ${{ inputs.java-distribution }} + java-early-access: ${{ inputs.java-early-access }} + java-toolchain: ${{ inputs.java-toolchain }} + java-version: ${{ inputs.java-version }} + - name: Build + id: build + if: ${{ inputs.publish == 'false' }} + shell: bash + run: ./gradlew build + - name: Publish + id: publish + if: ${{ inputs.publish == 'true' }} + shell: bash + run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository ${{ !startsWith(github.event.head_commit.message, 'Next development version') && 'build' || '' }} publishAllPublicationsToDeploymentRepository + - name: Read Version From gradle.properties + id: read-version + shell: bash + run: | + version=$(sed -n 's/version=\(.*\)/\1/p' gradle.properties) + echo "Version is $version" + echo "version=$version" >> $GITHUB_OUTPUT diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml new file mode 100644 index 000000000..03452537a --- /dev/null +++ b/.github/actions/create-github-release/action.yml @@ -0,0 +1,27 @@ +name: Create GitHub Release +description: 'Create the release on GitHub with a changelog' +inputs: + milestone: + description: 'Name of the GitHub milestone for which a release will be created' + required: true + pre-release: + description: 'Whether the release is a pre-release (a milestone or release candidate)' + required: false + default: 'false' + token: + description: 'Token to use for authentication with GitHub' + required: true +runs: + using: composite + steps: + - name: Generate Changelog + uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11 + with: + config-file: .github/actions/create-github-release/changelog-generator.yml + milestone: ${{ inputs.milestone }} + token: ${{ inputs.token }} + - name: Create GitHub Release + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.token }} + run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md ${{ inputs.pre-release == 'true' && '--prerelease' || '' }} diff --git a/.github/actions/create-github-release/changelog-generator.yml b/.github/actions/create-github-release/changelog-generator.yml new file mode 100644 index 000000000..0c1077fdf --- /dev/null +++ b/.github/actions/create-github-release/changelog-generator.yml @@ -0,0 +1,22 @@ +changelog: + sections: + - title: ":star: New Features" + labels: + - "type: enhancement" + - title: ":lady_beetle: Bug Fixes" + labels: + - "type: bug" + - "type: regression" + - title: ":notebook_with_decorative_cover: Documentation" + labels: + - "type: documentation" + - title: ":hammer: Dependency Upgrades" + sort: "title" + labels: + - "type: dependency-upgrade" + issues: + ports: + - label: "status: forward-port" + bodyExpression: 'Forward port of issue #(\d+).*' + - label: "status: back-port" + bodyExpression: 'Back port of issue #(\d+).*' diff --git a/.github/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml new file mode 100644 index 000000000..f9969cf31 --- /dev/null +++ b/.github/actions/prepare-gradle-build/action.yml @@ -0,0 +1,61 @@ +name: Prepare Gradle Build +description: 'Prepares a Gradle build. Sets up Java and Gradle and configures Gradle properties' +inputs: + cache-read-only: + description: 'Whether Gradle''s cache should be read only' + required: false + default: 'true' + develocity-access-key: + description: 'Access key for authentication with ge.spring.io' + required: false + java-distribution: + description: 'Java distribution to use' + required: false + default: 'liberica' + java-early-access: + description: 'Whether the Java version is in early access. When true, forces java-distribution to temurin' + required: false + default: 'false' + java-toolchain: + description: 'Whether a Java toolchain should be used' + required: false + default: 'false' + java-version: + description: 'Java version to use for the build' + required: false + default: '17' +runs: + using: composite + steps: + - name: Set Up Java + uses: actions/setup-java@v4 + with: + distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || (inputs.java-distribution || 'liberica') }} + java-version: | + ${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }} + ${{ inputs.java-toolchain == 'true' && '17' || '' }} + - name: Set Up Gradle With Read/Write Cache + if: ${{ inputs.cache-read-only == 'false' }} + uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 + with: + cache-read-only: false + develocity-access-key: ${{ inputs.develocity-access-key }} + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 + with: + develocity-access-key: ${{ inputs.develocity-access-key }} + - name: Configure Gradle Properties + shell: bash + run: | + mkdir -p $HOME/.gradle + echo 'systemProp.user.name=spring-builds+github' >> $HOME/.gradle/gradle.properties + echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties + - name: Configure Toolchain Properties + if: ${{ inputs.java-toolchain == 'true' }} + shell: bash + run: | + echo toolchainVersion=${{ inputs.java-version }} >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.auto-detect=false >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.auto-download=false >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', inputs.java-version) }} >> $HOME/.gradle/gradle.properties diff --git a/.github/actions/print-jvm-thread-dumps/action.yml b/.github/actions/print-jvm-thread-dumps/action.yml new file mode 100644 index 000000000..bcaebf367 --- /dev/null +++ b/.github/actions/print-jvm-thread-dumps/action.yml @@ -0,0 +1,17 @@ +name: Print JVM thread dumps +description: 'Prints a thread dump for all running JVMs' +runs: + using: composite + steps: + - if: ${{ runner.os == 'Linux' }} + shell: bash + run: | + for jvm_pid in $(jps -q -J-XX:+PerfDisableSharedMem); do + jcmd $jvm_pid Thread.print + done + - if: ${{ runner.os == 'Windows' }} + shell: powershell + run: | + foreach ($jvm_pid in $(jps -q -J-XX:+PerfDisableSharedMem)) { + jcmd $jvm_pid Thread.print + } diff --git a/.github/actions/send-notification/action.yml b/.github/actions/send-notification/action.yml new file mode 100644 index 000000000..b379e6789 --- /dev/null +++ b/.github/actions/send-notification/action.yml @@ -0,0 +1,39 @@ +name: Send Notification +description: 'Sends a Google Chat message as a notification of the job''s outcome' +inputs: + build-scan-url: + description: 'URL of the build scan to include in the notification' + required: false + run-name: + description: 'Name of the run to include in the notification' + required: false + default: ${{ format('{0} {1}', github.ref_name, github.job) }} + status: + description: 'Status of the job' + required: true + webhook-url: + description: 'Google Chat Webhook URL' + required: true +runs: + using: composite + steps: + - name: Prepare Variables + shell: bash + run: | + echo "BUILD_SCAN=${{ inputs.build-scan-url == '' && ' [build scan unavailable]' || format(' [<{0}|Build Scan>]', inputs.build-scan-url) }}" >> "$GITHUB_ENV" + echo "RUN_URL=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> "$GITHUB_ENV" + - name: Success Notification + if: ${{ inputs.status == 'success' }} + shell: bash + run: | + curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was successful ${{ env.BUILD_SCAN }}"}' || true + - name: Failure Notification + if: ${{ inputs.status == 'failure' }} + shell: bash + run: | + curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: " *<${{ env.RUN_URL }}|${{ inputs.run-name }}> failed* ${{ env.BUILD_SCAN }}"}' || true + - name: Cancel Notification + if: ${{ inputs.status == 'cancelled' }} + shell: bash + run: | + curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was cancelled"}' || true diff --git a/.github/actions/sync-to-maven-central/action.yml b/.github/actions/sync-to-maven-central/action.yml new file mode 100644 index 000000000..5a264aa2c --- /dev/null +++ b/.github/actions/sync-to-maven-central/action.yml @@ -0,0 +1,34 @@ +name: Sync to Maven Central +description: 'Syncs a release to Maven Central and waits for it to be available for use' +inputs: + central-token-password: + description: 'Password for authentication with central.sonatype.com' + required: true + central-token-username: + description: 'Username for authentication with central.sonatype.com' + required: true + jfrog-cli-config-token: + description: 'Config token for the JFrog CLI' + required: true + spring-restdocs-version: + description: 'Version of Spring REST Docs that is being synced to Central' + required: true +runs: + using: composite + steps: + - name: Set Up JFrog CLI + uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1 + env: + JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }} + - name: Download Release Artifacts + shell: bash + run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-restdocs-{0}', inputs.spring-restdocs-version) }};buildNumber=${{ github.run_number }}' + - name: Sync + uses: spring-io/central-publish-action@0cdd90d12e6876341e82860d951e1bcddc1e51b6 # v0.2.0 + with: + token: ${{ inputs.central-token-password }} + token-name: ${{ inputs.central-token-username }} + - name: Await + uses: ./.github/actions/await-http-resource + with: + url: ${{ format('https://repo.maven.apache.org/maven2/org/springframework/restdocs/spring-restdocs-core/{0}/spring-restdocs-core-{0}.jar', inputs.spring-restdocs-version) }} diff --git a/.github/actions/sync-to-maven-central/artifacts.spec b/.github/actions/sync-to-maven-central/artifacts.spec new file mode 100644 index 000000000..82049730c --- /dev/null +++ b/.github/actions/sync-to-maven-central/artifacts.spec @@ -0,0 +1,20 @@ +{ + "files": [ + { + "aql": { + "items.find": { + "$and": [ + { + "@build.name": "${buildName}", + "@build.number": "${buildNumber}", + "path": { + "$nmatch": "org/springframework/restdocs/spring-restdocs/*" + } + } + ] + } + }, + "target": "nexus/" + } + ] +} diff --git a/.github/dco.yml b/.github/dco.yml new file mode 100644 index 000000000..0c4b142e9 --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,2 @@ +require: + members: false diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml new file mode 100644 index 000000000..8850cec14 --- /dev/null +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -0,0 +1,45 @@ +name: Build and Deploy Snapshot +on: + push: + branches: + - main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + build-and-deploy-snapshot: + name: Build and Deploy Snapshot + if: ${{ github.repository == 'spring-projects/spring-restdocs' }} + runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} + steps: + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build and Publish + id: build-and-publish + uses: ./.github/actions/build + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + gradle-cache-read-only: false + publish: true + - name: Deploy + uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 + with: + artifact-properties: | + /**/spring-restdocs-*.zip::zip.type=docs,zip.deployed=false + build-name: 'spring-restdocs-4.0.x' + folder: 'deployment-repository' + password: ${{ secrets.ARTIFACTORY_PASSWORD }} + repository: ${{ 'libs-snapshot-local' }} + signing-key: ${{ secrets.GPG_PRIVATE_KEY }} + signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + uri: 'https://repo.spring.io' + username: ${{ secrets.ARTIFACTORY_USERNAME }} + - name: Send Notification + if: always() + uses: ./.github/actions/send-notification + with: + build-scan-url: ${{ steps.build-and-publish.outputs.build-scan-url }} + run-name: ${{ format('{0} | Linux | Java 17', github.ref_name) }} + status: ${{ job.status }} + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + outputs: + version: ${{ steps.build-and-publish.outputs.version }} diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml new file mode 100644 index 000000000..164c04dc2 --- /dev/null +++ b/.github/workflows/build-pull-request.yml @@ -0,0 +1,24 @@ +name: Build Pull Request +on: pull_request +permissions: + contents: read +jobs: + build: + name: Build Pull Request + if: ${{ github.repository == 'spring-projects/spring-restdocs' }} + runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} + steps: + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build + id: build + uses: ./.github/actions/build + - name: Print JVM Thread Dumps When Cancelled + if: cancelled() + uses: ./.github/actions/print-jvm-thread-dumps + - name: Upload Build Reports + if: failure() + uses: actions/upload-artifact@v4 + with: + name: build-reports + path: '**/build/reports/' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..4df789601 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CI +on: + push: + branches: + - main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + ci: + name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}' + if: ${{ github.repository == 'spring-projects/spring-restdocs' }} + runs-on: ${{ matrix.os.id }} + strategy: + fail-fast: false + matrix: + os: + - id: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} + name: Linux + - id: windows-latest + name: Windows + java: + - version: 17 + toolchain: false + - version: 21 + toolchain: false + - version: 24 + toolchain: false + exclude: + - os: + name: Linux + java: + version: 17 + steps: + - name: Prepare Windows runner + if: ${{ runner.os == 'Windows' }} + run: | + git config --global core.autocrlf true + git config --global core.longPaths true + Stop-Service -name Docker + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build + id: build + uses: ./.github/actions/build + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + gradle-cache-read-only: false + java-early-access: ${{ matrix.java.early-access || 'false' }} + java-distribution: ${{ matrix.java.distribution }} + java-toolchain: ${{ matrix.java.toolchain }} + java-version: ${{ matrix.java.version }} + - name: Send Notification + if: always() + uses: ./.github/actions/send-notification + with: + build-scan-url: ${{ steps.build.outputs.build-scan-url }} + run-name: ${{ format('{0} | {1} | Java {2}', github.ref_name, matrix.os.name, matrix.java.version) }} + status: ${{ job.status }} + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} diff --git a/.github/workflows/delete-staged-release.yml b/.github/workflows/delete-staged-release.yml new file mode 100644 index 000000000..56aff2b40 --- /dev/null +++ b/.github/workflows/delete-staged-release.yml @@ -0,0 +1,18 @@ +name: Delete Staged Release +on: + workflow_dispatch: + inputs: + build-version: + description: 'Version of the build to delete' + required: true +jobs: + delete-staged-release: + name: Delete Staged Release + runs-on: ubuntu-latest + steps: + - name: Set up JFrog CLI + uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1 + env: + JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + - name: Delete Build + run: jfrog rt delete --build spring-restdocs-${{ github.event.inputs.build-version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..5b69c73b8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,79 @@ +name: Release +on: + push: + tags: + - v3.0.[0-9]+ +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + build-and-stage-release: + name: Build and Stage Release + if: ${{ github.repository == 'spring-projects/spring-restdocs' }} + runs-on: ${{ vars.UBUNTU_MEDIUIM || 'ubuntu-latest' }} + steps: + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build and Publish + id: build-and-publish + uses: ./.github/actions/build + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + gradle-cache-read-only: false + publish: true + - name: Stage Release + uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 + with: + artifact-properties: | + /**/spring-restdocs-*.zip::zip.type=docs,zip.deployed=false + build-name: ${{ format('spring-restdocs-{0}', steps.build-and-publish.outputs.version) }} + folder: 'deployment-repository' + password: ${{ secrets.ARTIFACTORY_PASSWORD }} + repository: 'libs-staging-local' + signing-key: ${{ secrets.GPG_PRIVATE_KEY }} + signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + uri: 'https://repo.spring.io' + username: ${{ secrets.ARTIFACTORY_USERNAME }} + outputs: + version: ${{ steps.build-and-publish.outputs.version }} + sync-to-maven-central: + name: Sync to Maven Central + needs: + - build-and-stage-release + runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} + steps: + - name: Check Out Code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Sync to Maven Central + uses: ./.github/actions/sync-to-maven-central + with: + central-token-password: ${{ secrets.CENTRAL_TOKEN_PASSWORD }} + central-token-username: ${{ secrets.CENTRAL_TOKEN_USERNAME }} + jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }} + spring-restdocs-version: ${{ needs.build-and-stage-release.outputs.version }} + promote-release: + name: Promote Release + needs: + - build-and-stage-release + - sync-to-maven-central + runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} + steps: + - name: Set up JFrog CLI + uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1 + env: + JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + - name: Promote Open Source Build + run: jfrog rt build-promote ${{ format('spring-restdocs-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-release-local + create-github-release: + name: Create GitHub Release + needs: + - build-and-stage-release + - promote-release + runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }} + steps: + - name: Check Out Code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Create GitHub Release + uses: ./.github/actions/create-github-release + with: + milestone: ${{ needs.build-and-stage-release.outputs.version }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} diff --git a/.gitignore b/.gitignore index c508c0c30..c01583a59 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ bin build !buildSrc/src/main/groovy/org/springframework/restdocs/build/ target -.idea +.idea/* +!.idea/icon.svg *.iml \ No newline at end of file diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 000000000..5e6592046 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 000000000..bea2d5156 --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,3 @@ +# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +java=17.0.15-librca diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d7dcb3326..24e7b4254 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,63 +1,58 @@ # Contributing to Spring REST Docs -Spring REST Docs is released under the Apache 2.0 license. If you would like to -contribute something, or simply want to work with the code, this document should help you -to get started. +Spring REST Docs is released under the Apache 2.0 license. +If you would like to contribute something, or simply want to work with the code, this document should help you to get started. ## Code of conduct -This project adheres to the Contributor Covenant [code of conduct][1]. By participating, -you are expected to uphold this code. Please report unacceptable behavior to -spring-code-of-conduct@pivotal.io. +This project adheres to the Contributor Covenant [code of conduct][1]. By participating, you are expected to uphold this code. +Please report unacceptable behavior to spring-code-of-conduct@pivotal.io. -## Sign the contributor license agreement - -Before we accept a non-trivial patch or pull request we will need you to sign the -[contributor's license agreement (CLA)][2]. Signing the contributor's agreement does not -grant anyone commit rights to the main repository, but it does mean that we can accept -your contributions, and you will get an author credit if we do. +## Include a Signed-off-by Trailer +All commits must include a _Signed-off-by_ trailer at the end of each commit message to indicate that the contributor agrees to the [Developer Certificate of Origin (DCO)](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin). +For additional details, please refer to the ["Hello DCO, Goodbye CLA: Simplifying Contributions to Spring"](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring) blog post. ## Code conventions and housekeeping None of these is essential for a pull request, but they will all help -- Make sure all new `.java` files to have a simple Javadoc class comment with at least an - `@author` tag identifying you, and preferably at least a paragraph on what the class is - for. -- Add the ASF license header comment to all new `.java` files (copy from existing files - in the project) -- Add yourself as an `@author` to the .java files that you modify substantially (more - than cosmetic changes). +- Make sure all new `.java` files to have a simple Javadoc class comment with at least an `@author` tag identifying you, and preferably at least a paragraph on what the class is for +- Add the ASF license header comment to all new `.java` files (copy from existing files in the project) +- Add yourself as an `@author` to the .java files that you modify substantially (more than cosmetic changes) - Add some Javadocs - Add unit tests that covers and new or modified functionality -- Whenever possible, please rebase your branch against the current master (or other - target branch in the main project). -* When writing a commit message please follow [these conventions][3]. Also, if you are - fixing an existing issue please add `Fixes gh-nnn` at the end of the commit message - (where nnn is the issue number). +- Whenever possible, please rebase your branch against the current main (or other target branch in the project) +- When writing a commit message please follow [these conventions][3] + Also, if you are fixing an existing issue please add `Fixes gh-nnn` at the end of the commit message (where nnn is the issue number) ## Working with the code ### Building from source -To build the source you will need Java 7 or later. The code is built with Gradle: +To build the source you will need Java 17 or later. +The code is built with Gradle: ``` $ ./gradlew build ``` +To build the samples, run the following command: + +``` +$ ./gradlew buildSamples +``` + ### Importing into Eclipse -The project has Gradle's Eclipse plugin applied. Eclipse project and classpath metadata -can be generated by running the `eclipse` task: +The project has Gradle's Eclipse plugin applied. +Eclipse project and classpath metadata can be generated by running the `eclipse` task: ``` $ ./gradlew eclipse ``` -The project can then be imported into Eclipse using `File -> Import…` and then selecting -`General -> Existing Projects into Workspace`. +The project can then be imported into Eclipse using `File -> Import…` and then selecting `General -> Existing Projects into Workspace`. [1]: CODE_OF_CONDUCT.md [2]: https://cla.pivotal.io/sign/spring diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..823c1c8e9 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://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 + + 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. \ No newline at end of file diff --git a/README.md b/README.md index 562da7d2b..e3dec714d 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,12 @@ -# Spring REST Docs [![Build status][1]][2] +# Spring REST Docs [![Build status][1]][2] [![Revved up by Develocity][23]][24] ## Overview -The primary goal of this project is to make it easy to document RESTful services by -combining content that's been hand-written using [Asciidoctor][3] with auto-generated -examples produced with the [Spring MVC Test][4] framework. The result is intended to be -an easy-to-read user guide, akin to [GitHub's API documentation][5] for example, rather -than the fully automated, dense API documentation produced by tools like [Swagger][6]. +The primary goal of this project is to make it easy to document RESTful services by combining content that's been hand-written using [Asciidoctor][3] with auto-generated examples produced with the [Spring MVC Test][4] framework. +The result is intended to be an easy-to-read user guide, akin to [GitHub's API documentation][5] for example, rather than the fully automated, dense API documentation produced by tools like [Swagger][6]. -For a broader introduction see the Documenting RESTful APIs presentation. Both the -[slides][7] and a [video recording][8] are available. +For a broader introduction see the Documenting RESTful APIs presentation. +Both the [slides][7] and a [video recording][8] are available. ## Learning more @@ -17,7 +14,8 @@ To learn more about Spring REST Docs, please consult the [reference documentatio ## Building from source -You will need Java 7 or later to build Spring REST Docs. It is built using [Gradle][10]: +You will need Java 17 or later to build Spring REST Docs. +It is built using [Gradle][10]: ``` ./gradlew build @@ -29,23 +27,24 @@ Contributors to this project agree to uphold its [code of conduct][11]. There are several that you can contribute to Spring REST Docs: - - Open a [pull request][12]. Please see the [contributor guidelines][13] for details. - - Ask and answer questions on Stack Overflow using the [`spring-restdocs`][15] tag. - - Chat with fellow users [on Gitter][16]. + - Open a [pull request][12]. Please see the [contributor guidelines][13] for details + - Ask and answer questions on Stack Overflow using the [`spring-restdocs`][15] tag ## Third-party extensions | Name | Description | | ---- | ----------- | -| [restdocs-wiremock][17] | Auto-generate WireMock stubs as part of documenting your RESTful API | -| [restdocsext-jersey][18] | Enables Spring REST Docs to be used with [Jersey's test framework][19] | +| [restdocs-wiremock][16] | Auto-generate WireMock stubs as part of documenting your RESTful API | +| [restdocsext-jersey][17] | Enables Spring REST Docs to be used with [Jersey's test framework][18] | +| [spring-auto-restdocs][19] | Uses introspection and Javadoc to automatically document request and response parameters | +| [restdocs-api-spec][20] | A Spring REST Docs extension that adds API specification support. It currently supports [OpenAPI 2][21] and [OpenAPI 3][22] | ## Licence Spring REST Docs is open source software released under the [Apache 2.0 license][14]. -[1]: https://build.spring.io/plugins/servlet/buildStatusImage/SRD-PUB (Build status) -[2]: https://build.spring.io/browse/SRD-PUB +[1]: https://github.com/spring-projects/spring-restdocs/actions/workflows/build-and-deploy-snapshot.yml/badge.svg?branch=main (Build status) +[2]: https://github.com/spring-projects/spring-restdocs/actions/workflows/build-and-deploy-snapshot.yml [3]: https://asciidoctor.org [4]: https://docs.spring.io/spring-framework/docs/4.1.x/spring-framework-reference/htmlsingle/#spring-mvc-test-framework [5]: https://developer.github.com/v3/ @@ -59,7 +58,12 @@ Spring REST Docs is open source software released under the [Apache 2.0 license] [13]: CONTRIBUTING.md [14]: https://www.apache.org/licenses/LICENSE-2.0.html [15]: https://stackoverflow.com/tags/spring-restdocs -[16]: https://gitter.im/spring-projects/spring-restdocs -[17]: https://github.com/ePages-de/restdocs-wiremock -[18]: https://github.com/RESTDocsEXT/restdocsext-jersey -[19]: https://jersey.java.net/documentation/latest/test-framework.html +[16]: https://github.com/ePages-de/restdocs-wiremock +[17]: https://github.com/RESTDocsEXT/restdocsext-jersey +[18]: https://jersey.java.net/documentation/latest/test-framework.html +[19]: https://github.com/ScaCap/spring-auto-restdocs +[20]: https://github.com/ePages-de/restdocs-api-spec +[21]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md +[22]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md +[23]: https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A +[24]: https://ge.spring.io/scans?search.rootProjectNames=spring-restdocs diff --git a/build.gradle b/build.gradle index f8e43973c..32bea2f44 100644 --- a/build.gradle +++ b/build.gradle @@ -1,192 +1,123 @@ -buildscript { - repositories { - mavenCentral() - maven { url 'https://repo.spring.io/plugins-release' } - maven { url 'https://plugins.gradle.org/m2/' } - } - dependencies { - classpath 'io.spring.gradle:dependency-management-plugin:0.5.5.RELEASE' - classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' - classpath 'io.spring.gradle:spring-io-plugin:0.0.5.RELEASE' - classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:1.2' - } +plugins { + id 'base' + id 'io.spring.javaformat' version "$javaFormatVersion" apply false + id 'io.spring.nohttp' version '0.0.11' apply false + id 'maven-publish' } allprojects { - group = 'org.springframework.restdocs' + group = "org.springframework.restdocs" repositories { mavenCentral() + maven { + url = "https://repo.spring.io/milestone" + content { + includeGroup "io.micrometer" + includeGroup "io.projectreactor" + includeGroup "org.springframework" + } + } + if (version.endsWith('-SNAPSHOT')) { + maven { url = "https://repo.spring.io/snapshot" } + } } } -apply plugin: 'samples' -apply plugin: 'org.sonarqube' - -sonarqube { - properties { - property 'sonar.branch', '1.1.x' - property 'sonar.jacoco.reportPath', "${buildDir.name}/jacoco.exec" - property 'sonar.java.coveragePlugin', 'jacoco' - property 'sonar.links.ci', 'https://build.spring.io/browse/SRD' - property 'sonar.links.homepage', 'https://github.com/spring-projects/spring-restdocs' - property 'sonar.links.issue', 'https://github.com/spring-projects/spring-restdocs' - property 'sonar.links.scm', 'https://github.com/spring-projects/spring-restdocs' - } +apply plugin: "io.spring.nohttp" +apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" + +nohttp { + source.exclude "buildSrc/.gradle/**" + source.exclude "**/build/**" + source.exclude "**/target/**" } ext { - springVersion = '4.2.5.RELEASE' javadocLinks = [ - 'https://docs.oracle.com/javase/8/docs/api/', - "https://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", - 'https://docs.jboss.org/hibernate/stable/beanvalidation/api/', - 'https://docs.jboss.org/hibernate/stable/validator/api/' + "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/", + "https://docs.jboss.org/hibernate/validator/9.0/api/", + "https://jakarta.ee/specifications/bean-validation/3.1/apidocs/" ] as String[] } -subprojects { - apply plugin: 'io.spring.dependency-management' - apply plugin: 'java' - apply plugin: 'eclipse' - apply plugin: 'propdeps' - apply plugin: 'propdeps-eclipse' - apply plugin: 'propdeps-maven' - apply plugin: 'maven' - - sourceCompatibility = 1.7 - targetCompatibility = 1.7 - - dependencyManagement { - imports { - mavenBom "org.springframework:spring-framework-bom:$springVersion" - } - dependencies { - dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6.1' - dependency 'com.jayway.restassured:rest-assured:2.8.0' - dependency 'com.samskivert:jmustache:1.12' - dependency 'commons-codec:commons-codec:1.10' - dependency 'javax.servlet:javax.servlet-api:3.1.0' - dependency 'javax.validation:validation-api:1.1.0.Final' - dependency 'junit:junit:4.12' - dependency 'org.hamcrest:hamcrest-core:1.3' - dependency 'org.hamcrest:hamcrest-library:1.3' - dependency 'org.hibernate:hibernate-validator:5.2.2.Final' - dependency 'org.mockito:mockito-core:1.10.19' - dependency 'org.springframework.hateoas:spring-hateoas:0.19.0.RELEASE' - dependency 'org.jacoco:org.jacoco.agent:0.7.7.201606060606' - } - } - - test { - testLogging { - exceptionFormat "full" - } - } - - eclipseJdt { - inputFile = rootProject.file('config/eclipse/org.eclipse.jdt.core.prefs') - doLast { - project.file('.settings/org.eclipse.jdt.ui.prefs').withWriter { writer -> - writer << rootProject.file('config/eclipse/org.eclipse.jdt.ui.prefs').text - } - } - } - - compileJava { - options.compilerArgs = [ '-Xlint:deprecation', '-Xlint:-options', '-Werror' ] - } - +check { + dependsOn checkstyleNohttp } -configure(subprojects - project(":docs")) { subproject -> - - apply plugin: 'checkstyle' - apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" - - if (project.hasProperty('platformVersion')) { - apply plugin: 'spring-io' +subprojects { subproject -> + plugins.withType(JavaPlugin) { + subproject.apply plugin: "io.spring.javaformat" + subproject.apply plugin: "checkstyle" - repositories { - maven { url "https://repo.spring.io/libs-snapshot" } + java { + sourceCompatibility = 17 + targetCompatibility = 17 } - dependencyManagement { - springIoTestRuntime { - imports { - mavenBom "io.spring.platform:platform-bom:${platformVersion}" - } + configurations { + all { + resolutionStrategy.cacheChangingModulesFor 0, "minutes" } + internal { + canBeConsumed = false + canBeResolved = false + } + compileClasspath.extendsFrom(internal) + runtimeClasspath.extendsFrom(internal) + testCompileClasspath.extendsFrom(internal) + testRuntimeClasspath.extendsFrom(internal) } - } - - checkstyle { - configFile = rootProject.file('config/checkstyle/checkstyle.xml') - configProperties = [ 'checkstyle.config.dir' : rootProject.file('config/checkstyle') ] - toolVersion = '6.10.1' - } - - configurations { - jacoco - } - - dependencies { - jacoco 'org.jacoco:org.jacoco.agent::runtime' - } - javadoc { - description = "Generates project-level javadoc for use in -javadoc jar" - options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED - options.author = true - options.header = "Spring REST Docs $version" - options.docTitle = "${options.header} API" - options.links = javadocLinks - options.addStringOption '-quiet' - } - - task sourcesJar(type: Jar) { - classifier = 'sources' - from project.sourceSets.main.allSource - } - - task javadocJar(type: Jar) { - classifier = "javadoc" - from javadoc - } + test { + testLogging { + exceptionFormat = "full" + } + } - artifacts { - archives sourcesJar - archives javadocJar - } -} + tasks.withType(JavaCompile) { + options.compilerArgs = [ "-Werror", "-Xlint:unchecked", "-Xlint:deprecation", "-Xlint:rawtypes", "-Xlint:varargs", "-Xlint:options" ] + options.encoding = "UTF-8" + } -samples { - dependOn 'spring-restdocs-core:install' - dependOn 'spring-restdocs-mockmvc:install' - dependOn 'spring-restdocs-restassured:install' + tasks.withType(Test) { + maxHeapSize = "1024M" + } - restNotesGrails { - workingDir "$projectDir/samples/rest-notes-grails" - } + checkstyle { + configFile = rootProject.file("config/checkstyle/checkstyle.xml") + configProperties = [ "checkstyle.config.dir" : rootProject.file("config/checkstyle") ] + toolVersion = "10.12.4" + } - restNotesSpringHateoas { - workingDir "$projectDir/samples/rest-notes-spring-hateoas" - } + dependencies { + checkstyle("com.puppycrawl.tools:checkstyle:${checkstyle.toolVersion}") + checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}") + } - restNotesSpringDataRest { - workingDir "$projectDir/samples/rest-notes-spring-data-rest" - } + plugins.withType(MavenPublishPlugin) { + javadoc { + description = "Generates project-level javadoc for use in -javadoc jar" + options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED + options.author = true + options.header = "Spring REST Docs $version" + options.docTitle = "${options.header} API" + options.links = javadocLinks + options.addStringOption "-quiet" + options.encoding = "UTF-8" + options.source = "17" + } - testNg { - workingDir "$projectDir/samples/testng" + java { + withJavadocJar() + withSourcesJar() + } + } } - - restAssured { - workingDir "$projectDir/samples/rest-assured" + plugins.withType(MavenPublishPlugin) { + subproject.apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" } - - slate { - workingDir "$projectDir/samples/rest-notes-slate" - build false + tasks.withType(GenerateModuleMetadata) { + enabled = false } } @@ -194,57 +125,50 @@ samples { task api (type: Javadoc) { group = "Documentation" description = "Generates aggregated Javadoc API documentation." - dependsOn { - subprojects.collect { - it.tasks.getByName("jar") + project.rootProject.gradle.projectsEvaluated { + Set excludedProjects = ['spring-restdocs-asciidoctor'] + Set publishedProjects = rootProject.subprojects.findAll { it != project} + .findAll { it.plugins.hasPlugin(JavaPlugin) && it.plugins.hasPlugin(MavenPublishPlugin) } + .findAll { !excludedProjects.contains(it.name) } + .findAll { !it.name.startsWith('spring-boot-starter') } + dependsOn publishedProjects.javadoc + source publishedProjects.javadoc.source + classpath = project.files(publishedProjects.javadoc.classpath) + destinationDir = project.file "${buildDir}/docs/javadoc" + options { + author = true + docTitle = "Spring REST Docs ${project.version} API" + encoding = "UTF-8" + memberLevel = "protected" + outputLevel = "quiet" + source = "17" + splitIndex = true + use = true + windowTitle = "Spring REST Docs ${project.version} API" } } - options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED - options.author = true - options.header = "Spring REST Docs $version" - options.splitIndex = true - options.links = javadocLinks - options.addStringOption '-quiet' - - source subprojects.collect { project -> - project.sourceSets.main.allJava - } - - destinationDir = new File(buildDir, "api") - - doFirst { - classpath = files(subprojects.collect { it.sourceSets.main.compileClasspath }) - } } -task docsZip(type: Zip, dependsOn: [':docs:asciidoctor', ':api', ':buildSamples']) { - group = 'Distribution' - baseName = 'spring-restdocs' - classifier = 'docs' - description = "Builds -${classifier} archive containing API and reference documentation" - destinationDir = file("${project.buildDir}/distributions") +task docsZip(type: Zip, dependsOn: [":docs:asciidoctor", ":api"]) { + group = "Distribution" + archiveBaseName = "spring-restdocs" + archiveClassifier = "docs" + description = "Builds -${archiveClassifier} archive containing API and reference documentation" + destinationDirectory = file("${project.buildDir}/distributions") - from(project.tasks.findByPath(':docs:asciidoctor')) { - into 'reference' + from(project.tasks.findByPath(":docs:asciidoctor")) { + into "reference/htmlsingle" } from(api) { - into 'api' - } - - from(file('samples/rest-notes-spring-hateoas/build/asciidoc/html5')) { - into 'samples/restful-notes' - } - - from(file('samples/rest-notes-slate/build/docs')) { - into 'samples/slate' + into "api" } } -configurations { - archives +publishing { + publications { + maven(MavenPublication) { + artifact docsZip + } + } } - -artifacts { - archives docsZip -} \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 000000000..6746f1805 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,3 @@ +plugins { + id "java-gradle-plugin" +} diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy deleted file mode 100644 index c2dcc6447..000000000 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2014-2017 the original author or 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. - */ - -package org.springframework.restdocs.build - -import org.gradle.api.GradleException -import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.tasks.Exec -import org.gradle.api.tasks.Copy -import org.gradle.api.tasks.GradleBuild - -public class SampleBuildConfigurer { - - private final String name - - private String workingDir - - private boolean build = true - - SampleBuildConfigurer(String name) { - this.name = name - } - - void workingDir(String workingDir) { - this.workingDir = workingDir - } - - void build(boolean build) { - this.build = build - } - - Task createTask(Project project, Object... dependencies) { - File sampleDir = new File(this.workingDir).absoluteFile - - Task sampleBuild = project.tasks.create name - sampleBuild.description = "Builds the ${name} sample" - sampleBuild.group = "Build" - - if (new File(sampleDir, 'build.gradle').isFile()) { - Task gradleVersionsUpdate = createGradleVersionsUpdate(project) - sampleBuild.dependsOn gradleVersionsUpdate - if (build) { - Task gradleBuild = createGradleBuild(project, dependencies) - Task verifyIncludesTask = createVerifyIncludes(project, new File(sampleDir, 'build/asciidoc')) - verifyIncludesTask.dependsOn gradleBuild - sampleBuild.dependsOn verifyIncludesTask - gradleBuild.dependsOn gradleVersionsUpdate - } - } - else if (new File(sampleDir, 'pom.xml').isFile()) { - Task mavenVersionsUpdate = createMavenVersionsUpdate(project) - sampleBuild.dependsOn mavenVersionsUpdate - if (build) { - Task mavenBuild = createMavenBuild(project, sampleDir, dependencies) - Task verifyIncludesTask = createVerifyIncludes(project, new File(sampleDir, 'target/generated-docs')) - verifyIncludesTask.dependsOn(mavenBuild) - sampleBuild.dependsOn verifyIncludesTask - mavenBuild.dependsOn mavenVersionsUpdate - } - } - else { - throw new IllegalStateException("No pom.xml or build.gradle was found in $sampleDir") - } - return sampleBuild - } - - private Task createMavenVersionsUpdate(Project project) { - Task mavenVersionsUpdate = project.tasks.create "${name}MavenVersionUpdates" - mavenVersionsUpdate.doFirst { - replaceVersion(new File(this.workingDir, 'pom.xml'), - '.*', - "${project.version}") - } - return mavenVersionsUpdate - } - - private Task createGradleVersionsUpdate(Project project) { - Task gradleVersionsUpdate = project.tasks.create "${name}GradleVersionUpdates" - gradleVersionsUpdate.doFirst { - replaceVersion(new File(this.workingDir, 'build.gradle'), - "ext\\['spring-restdocs.version'\\] = '.*'", - "ext['spring-restdocs.version'] = '${project.version}'") - replaceVersion(new File(this.workingDir, 'build.gradle'), - "restDocsVersion = \".*\"", - "restDocsVersion = \"${project.version}\"") - } - return gradleVersionsUpdate - } - - private Task createMavenBuild(Project project, File sampleDir, Object... dependencies) { - Task mavenBuild = project.tasks.create("${name}Maven", Exec) - mavenBuild.description = "Builds the ${name} sample with Maven" - mavenBuild.group = "Build" - mavenBuild.workingDir = this.workingDir - mavenBuild.commandLine = [isWindows() ? "${sampleDir.absolutePath}/mvnw.cmd" : './mvnw', 'clean', 'package'] - mavenBuild.dependsOn dependencies - return mavenBuild - } - - private boolean isWindows() { - return File.separatorChar == '\\' - } - - private Task createGradleBuild(Project project, Object... dependencies) { - Task gradleBuild = project.tasks.create("${name}Gradle", GradleBuild) - gradleBuild.description = "Builds the ${name} sample with Gradle" - gradleBuild.group = "Build" - gradleBuild.dir = this.workingDir - gradleBuild.tasks = ['clean', 'build'] - gradleBuild.dependsOn dependencies - return gradleBuild - } - - private void replaceVersion(File target, String pattern, String replacement) { - def lines = target.readLines() - target.withWriter { writer -> - lines.each { line -> - writer.println(line.replaceAll(pattern, replacement)) - } - } - } - - private Task createVerifyIncludes(Project project, File buildDir) { - Task verifyIncludesTask = project.tasks.create("${name}VerifyIncludes") - verifyIncludesTask.description = "Verifies the includes in the ${name} sample" - verifyIncludesTask << { - Map unprocessedIncludes = [:] - buildDir.eachFileRecurse { file -> - if (file.name.endsWith('.html')) { - file.eachLine { line -> - if (line.contains(new File(this.workingDir).absolutePath)) { - unprocessedIncludes.get(file, []).add(line) - } - } - } - } - if (unprocessedIncludes) { - StringWriter message = new StringWriter() - PrintWriter writer = new PrintWriter(message) - writer.println 'Found unprocessed includes:' - unprocessedIncludes.each { file, lines -> - writer.println " ${file}:" - lines.each { line -> writer.println " ${line}" } - } - throw new GradleException(message.toString()) - } - } - return verifyIncludesTask - } -} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesExtension.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesExtension.groovy deleted file mode 100644 index 3a409801f..000000000 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesExtension.groovy +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -package org.springframework.restdocs.build - -import org.gradle.api.Project -import org.gradle.api.Task - -public class SamplesExtension { - - Project Project - - Task buildSamplesTask - - Object[] dependencies = [] - - SamplesExtension(Project project, Task buildSamplesTask) { - this.project = project - this.buildSamplesTask = buildSamplesTask - } - - void dependOn(Object... paths) { - this.dependencies += paths - } - - def methodMissing(String name, args) { - SampleBuildConfigurer configurer = new SampleBuildConfigurer(name) - Closure closure = args[0] - closure.delegate = configurer - closure.resolveStrategy = Closure.DELEGATE_FIRST - closure.call() - Task task = configurer.createTask(this.project, this.dependencies) - this.buildSamplesTask.dependsOn task - } - -} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesPlugin.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesPlugin.groovy deleted file mode 100644 index ca3cf49d4..000000000 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SamplesPlugin.groovy +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -package org.springframework.restdocs.build - -import org.gradle.api.Plugin -import org.gradle.api.Project - -public class SamplesPlugin implements Plugin { - - public void apply(Project project) { - def buildSamplesTask = project.tasks.create('buildSamples') - buildSamplesTask.description = 'Builds the configured samples' - buildSamplesTask.group = 'Build' - project.extensions.create('samples', SamplesExtension, project, buildSamplesTask) - } - -} \ No newline at end of file diff --git a/buildSrc/src/main/java/org/springframework/restdocs/build/optional/OptionalDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/restdocs/build/optional/OptionalDependenciesPlugin.java new file mode 100644 index 000000000..cb8642785 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/restdocs/build/optional/OptionalDependenciesPlugin.java @@ -0,0 +1,70 @@ +/* + * Copyright 2014-2024 the original author or 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. + */ + +package org.springframework.restdocs.build.optional; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.attributes.Usage; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.javadoc.Javadoc; +import org.gradle.plugins.ide.eclipse.EclipsePlugin; +import org.gradle.plugins.ide.eclipse.model.EclipseModel; + +/** + * A {@code Plugin} that adds support for Maven-style optional dependencies. Creates a new + * {@code optional} configuration. The {@code optional} configuration is part of the + * project's compile and runtime classpath's but does not affect the classpath of + * dependent projects. + * + * @author Andy Wilkinson + */ +public class OptionalDependenciesPlugin implements Plugin { + + /** + * Name of the {@code optional} configuration. + */ + public static final String OPTIONAL_CONFIGURATION_NAME = "optional"; + + @Override + public void apply(Project project) { + Configuration optional = project.getConfigurations().create(OPTIONAL_CONFIGURATION_NAME); + project.getConfigurations().all((configuration) -> { + if (configuration.getName().startsWith("testRuntimeClasspath_") || configuration.getName().startsWith("testCompileClasspath_")) { + configuration.extendsFrom(optional); + } + }); + optional.attributes((attributes) -> attributes.attribute(Usage.USAGE_ATTRIBUTE, + project.getObjects().named(Usage.class, Usage.JAVA_RUNTIME))); + project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> { + SourceSetContainer sourceSets = project.getExtensions().getByType(JavaPluginExtension.class) + .getSourceSets(); + sourceSets.all((sourceSet) -> { + sourceSet.setCompileClasspath(sourceSet.getCompileClasspath().plus(optional)); + sourceSet.setRuntimeClasspath(sourceSet.getRuntimeClasspath().plus(optional)); + }); + project.getTasks().withType(Javadoc.class) + .all((javadoc) -> javadoc.setClasspath(javadoc.getClasspath().plus(optional))); + }); + project.getPlugins().withType(EclipsePlugin.class, + (eclipsePlugin) -> project.getExtensions().getByType(EclipseModel.class) + .classpath((classpath) -> classpath.getPlusConfigurations().add(optional))); + } + +} diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/optional-dependencies.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/optional-dependencies.properties new file mode 100644 index 000000000..3a981c6fb --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/gradle-plugins/optional-dependencies.properties @@ -0,0 +1 @@ +implementation-class:org.springframework.restdocs.build.optional.OptionalDependenciesPlugin diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/samples.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/samples.properties deleted file mode 100644 index ded5899c5..000000000 --- a/buildSrc/src/main/resources/META-INF/gradle-plugins/samples.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class: org.springframework.restdocs.build.SamplesPlugin \ No newline at end of file diff --git a/config/checkstyle/checkstyle-header.txt b/config/checkstyle/checkstyle-header.txt deleted file mode 100644 index 39429ee64..000000000 --- a/config/checkstyle/checkstyle-header.txt +++ /dev/null @@ -1,17 +0,0 @@ -^\Q/*\E$ -^\Q * Copyright \E20\d\d\-20\d\d\Q the original author or authors.\E$ -^\Q *\E$ -^\Q * Licensed under the Apache License, Version 2.0 (the "License");\E$ -^\Q * you may not use this file except in compliance with the License.\E$ -^\Q * You may obtain a copy of the License at\E$ -^\Q *\E$ -^\Q * https://www.apache.org/licenses/LICENSE-2.0\E$ -^\Q *\E$ -^\Q * Unless required by applicable law or agreed to in writing, software\E$ -^\Q * distributed under the License is distributed on an "AS IS" BASIS,\E$ -^\Q * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\E$ -^\Q * See the License for the specific language governing permissions and\E$ -^\Q * limitations under the License.\E$ -^\Q */\E$ -^$ -^.*$ diff --git a/config/checkstyle/checkstyle-import-control.xml b/config/checkstyle/checkstyle-import-control.xml deleted file mode 100644 index 7bb744d36..000000000 --- a/config/checkstyle/checkstyle-import-control.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index 7c71fd49b..85f240e26 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -1,8 +1,8 @@ + "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN" + "https://checkstyle.org/dtds/suppressions_1_2.dtd"> - - + + diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index ec62f2ff3..612ebd174 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -1,156 +1,17 @@ - - - - - - - - - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - - - - + diff --git a/config/eclipse/org.eclipse.jdt.core.prefs b/config/eclipse/org.eclipse.jdt.core.prefs deleted file mode 100644 index 62ded11bf..000000000 --- a/config/eclipse/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,391 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.codeComplete.argumentPrefixes= -org.eclipse.jdt.core.codeComplete.argumentSuffixes= -org.eclipse.jdt.core.codeComplete.fieldPrefixes= -org.eclipse.jdt.core.codeComplete.fieldSuffixes= -org.eclipse.jdt.core.codeComplete.localPrefixes= -org.eclipse.jdt.core.codeComplete.localSuffixes= -org.eclipse.jdt.core.codeComplete.staticFieldPrefixes= -org.eclipse.jdt.core.codeComplete.staticFieldSuffixes= -org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes= -org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes= -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.7 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.doc.comment.support=enabled -org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.autoboxing=ignore -org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning -org.eclipse.jdt.core.compiler.problem.deadCode=warning -org.eclipse.jdt.core.compiler.problem.deprecation=warning -org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled -org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled -org.eclipse.jdt.core.compiler.problem.discouragedReference=warning -org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore -org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled -org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore -org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning -org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning -org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled -org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning -org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore -org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore -org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning -org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled -org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=disabled -org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled -org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=default -org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore -org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning -org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore -org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore -org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore -org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled -org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=public -org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=all_standard_tags -org.eclipse.jdt.core.compiler.problem.missingJavadocTags=warning -org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled -org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled -org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=default -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled -org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore -org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore -org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning -org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning -org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore -org.eclipse.jdt.core.compiler.problem.nullReference=ignore -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore -org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning -org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning -org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning -org.eclipse.jdt.core.compiler.processAnnotations=disabled -org.eclipse.jdt.core.compiler.source=1.7 -org.eclipse.jdt.core.formatter.align_type_members_on_columns=false -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_assignment=0 -org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 -org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 -org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 -org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 -org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 -org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 -org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 -org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=0 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 -org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 -org.eclipse.jdt.core.formatter.blank_lines_before_method=1 -org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 -org.eclipse.jdt.core.formatter.blank_lines_before_package=0 -org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 -org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 -org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false -org.eclipse.jdt.core.formatter.comment.format_block_comments=true -org.eclipse.jdt.core.formatter.comment.format_header=false -org.eclipse.jdt.core.formatter.comment.format_html=true -org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true -org.eclipse.jdt.core.formatter.comment.format_line_comments=true -org.eclipse.jdt.core.formatter.comment.format_source_code=false -org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true -org.eclipse.jdt.core.formatter.comment.indent_root_tags=false -org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=do not insert -org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert -org.eclipse.jdt.core.formatter.comment.line_length=90 -org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true -org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true -org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false -org.eclipse.jdt.core.formatter.compact_else_if=true -org.eclipse.jdt.core.formatter.continuation_indentation=2 -org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 -org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off -org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false -org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true -org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_empty_lines=false -org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true -org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false -org.eclipse.jdt.core.formatter.indentation.size=4 -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert -org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert -org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert -org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.join_lines_in_comments=true -org.eclipse.jdt.core.formatter.join_wrapped_lines=true -org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false -org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false -org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false -org.eclipse.jdt.core.formatter.lineSplit=90 -org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false -org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false -org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 -org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 -org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true -org.eclipse.jdt.core.formatter.tabulation.char=tab -org.eclipse.jdt.core.formatter.tabulation.size=4 -org.eclipse.jdt.core.formatter.use_on_off_tags=true -org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false -org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true -org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true -org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true -org.eclipse.jdt.core.javaFormatter=org.springframework.ide.eclipse.jdt.formatter.javaformatter diff --git a/config/eclipse/org.eclipse.jdt.ui.prefs b/config/eclipse/org.eclipse.jdt.ui.prefs deleted file mode 100644 index d9bb135cd..000000000 --- a/config/eclipse/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,125 +0,0 @@ -cleanup.add_default_serial_version_id=true -cleanup.add_generated_serial_version_id=false -cleanup.add_missing_annotations=true -cleanup.add_missing_deprecated_annotations=true -cleanup.add_missing_methods=false -cleanup.add_missing_nls_tags=false -cleanup.add_missing_override_annotations=true -cleanup.add_missing_override_annotations_interface_methods=true -cleanup.add_serial_version_id=false -cleanup.always_use_blocks=true -cleanup.always_use_parentheses_in_expressions=false -cleanup.always_use_this_for_non_static_field_access=true -cleanup.always_use_this_for_non_static_method_access=false -cleanup.convert_functional_interfaces=false -cleanup.convert_to_enhanced_for_loop=false -cleanup.correct_indentation=false -cleanup.format_source_code=true -cleanup.format_source_code_changes_only=false -cleanup.insert_inferred_type_arguments=false -cleanup.make_local_variable_final=false -cleanup.make_parameters_final=false -cleanup.make_private_fields_final=false -cleanup.make_type_abstract_if_missing_method=false -cleanup.make_variable_declarations_final=false -cleanup.never_use_blocks=false -cleanup.never_use_parentheses_in_expressions=true -cleanup.organize_imports=true -cleanup.qualify_static_field_accesses_with_declaring_class=false -cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -cleanup.qualify_static_member_accesses_with_declaring_class=true -cleanup.qualify_static_method_accesses_with_declaring_class=false -cleanup.remove_private_constructors=true -cleanup.remove_redundant_type_arguments=true -cleanup.remove_trailing_whitespaces=true -cleanup.remove_trailing_whitespaces_all=true -cleanup.remove_trailing_whitespaces_ignore_empty=false -cleanup.remove_unnecessary_casts=true -cleanup.remove_unnecessary_nls_tags=false -cleanup.remove_unused_imports=true -cleanup.remove_unused_local_variables=false -cleanup.remove_unused_private_fields=true -cleanup.remove_unused_private_members=false -cleanup.remove_unused_private_methods=true -cleanup.remove_unused_private_types=true -cleanup.sort_members=false -cleanup.sort_members_all=false -cleanup.use_anonymous_class_creation=false -cleanup.use_blocks=true -cleanup.use_blocks_only_for_return_and_throw=false -cleanup.use_lambda=true -cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=false -cleanup.use_this_for_non_static_field_access_only_if_necessary=false -cleanup.use_this_for_non_static_method_access=false -cleanup.use_this_for_non_static_method_access_only_if_necessary=true -cleanup.use_type_arguments=false -cleanup_profile=_Spring REST Docs Cleanup Conventions -cleanup_settings_version=2 -eclipse.preferences.version=1 -editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_Spring REST Docs Java Conventions -formatter_settings_version=12 -org.eclipse.jdt.ui.exception.name=e -org.eclipse.jdt.ui.gettersetter.use.is=true -org.eclipse.jdt.ui.ignorelowercasenames=true -org.eclipse.jdt.ui.importorder=java;javax;;org.springframework;\#; -org.eclipse.jdt.ui.javadoc=true -org.eclipse.jdt.ui.keywordthis=false -org.eclipse.jdt.ui.ondemandthreshold=9999 -org.eclipse.jdt.ui.overrideannotation=true -org.eclipse.jdt.ui.staticondemandthreshold=9999 -org.eclipse.jdt.ui.text.custom_code_templates= -sp_cleanup.add_default_serial_version_id=true -sp_cleanup.add_generated_serial_version_id=false -sp_cleanup.add_missing_annotations=true -sp_cleanup.add_missing_deprecated_annotations=true -sp_cleanup.add_missing_methods=false -sp_cleanup.add_missing_nls_tags=false -sp_cleanup.add_missing_override_annotations=true -sp_cleanup.add_missing_override_annotations_interface_methods=true -sp_cleanup.add_serial_version_id=false -sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=true -sp_cleanup.always_use_this_for_non_static_field_access=true -sp_cleanup.always_use_this_for_non_static_method_access=false -sp_cleanup.convert_to_enhanced_for_loop=false -sp_cleanup.correct_indentation=false -sp_cleanup.format_source_code=true -sp_cleanup.format_source_code_changes_only=false -sp_cleanup.make_local_variable_final=false -sp_cleanup.make_parameters_final=false -sp_cleanup.make_private_fields_final=false -sp_cleanup.make_type_abstract_if_missing_method=false -sp_cleanup.make_variable_declarations_final=false -sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=false -sp_cleanup.on_save_use_additional_actions=true -sp_cleanup.organize_imports=true -sp_cleanup.qualify_static_field_accesses_with_declaring_class=false -sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_with_declaring_class=true -sp_cleanup.qualify_static_method_accesses_with_declaring_class=false -sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_trailing_whitespaces=true -sp_cleanup.remove_trailing_whitespaces_all=true -sp_cleanup.remove_trailing_whitespaces_ignore_empty=false -sp_cleanup.remove_unnecessary_casts=true -sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=true -sp_cleanup.remove_unused_local_variables=false -sp_cleanup.remove_unused_private_fields=true -sp_cleanup.remove_unused_private_members=false -sp_cleanup.remove_unused_private_methods=true -sp_cleanup.remove_unused_private_types=true -sp_cleanup.sort_members=false -sp_cleanup.sort_members_all=false -sp_cleanup.use_blocks=true -sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_parentheses_in_expressions=false -sp_cleanup.use_this_for_non_static_field_access=true -sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false -sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/docs/build.gradle b/docs/build.gradle index 2727576dd..fefe9a5fa 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -1,29 +1,51 @@ plugins { - id 'org.asciidoctor.convert' version '1.5.3' + id "org.asciidoctor.jvm.convert" version "4.0.4" + id "java-library" } -repositories { - maven { - url 'https://repo.spring.io/release' - } +configurations { + asciidoctorExt } dependencies { - asciidoctor 'io.spring.asciidoctor:spring-asciidoctor-extensions:0.1.1.RELEASE' - testCompile project(':spring-restdocs-mockmvc') - testCompile project(':spring-restdocs-restassured') - testCompile 'javax.validation:validation-api' - testCompile 'junit:junit' - testCompile 'org.testng:testng:6.9.10' + asciidoctorExt("io.spring.asciidoctor.backends:spring-asciidoctor-backends:0.0.5") + + internal(platform(project(":spring-restdocs-platform"))) + internal(enforcedPlatform("org.springframework:spring-framework-bom:$springFrameworkVersion")) + + testImplementation(project(":spring-restdocs-mockmvc")) + testImplementation(project(":spring-restdocs-restassured")) + testImplementation(project(":spring-restdocs-webtestclient")) + testImplementation("jakarta.servlet:jakarta.servlet-api") + testImplementation("jakarta.validation:jakarta.validation-api") + testImplementation("org.testng:testng:6.9.10") + testImplementation("org.junit.jupiter:junit-jupiter-api") } tasks.findByPath("artifactoryPublish")?.enabled = false +tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { + baseDirFollowsSourceDir() +} + +jar { + enabled = false +} + +javadoc { + enabled = false +} + asciidoctor { + configurations 'asciidoctorExt' sources { - include 'index.adoc' + include "index.adoc" } - attributes 'revnumber': project.version, - 'branch-or-tag': project.version.endsWith('SNAPSHOT') ? '1.1.x': "v${project.version}" + attributes "revnumber": project.version, + "spring-Framework-version": springFrameworkVersion, + "branch-or-tag": project.version.endsWith("SNAPSHOT") ? "main": "v${project.version}" inputs.files(sourceSets.test.java) -} \ No newline at end of file + outputOptions { + backends "spring-html" + } +} diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index 178ac7b07..24c23a47e 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -1,17 +1,21 @@ [[configuration]] == Configuration +This section covers how to configure Spring REST Docs. + [[configuration-uris]] === Documented URIs -NOTE: As REST Assured tests a service by making actual HTTP requests, the documented -URIs cannot be customized in this way. You should use the -<> instead. +This section covers configuring documented URIs. + + + +[[configuration-uris-mockmvc]] +==== MockMvc URI Customization -When using MockMvc, the default configuration for URIs documented by Spring REST Docs is: +When using MockMvc, the default configuration for URIs documented by Spring REST Docs is as follows: |=== |Setting |Default @@ -26,27 +30,53 @@ When using MockMvc, the default configuration for URIs documented by Spring REST |`8080` |=== -This configuration is applied by `MockMvcRestDocumentationConfigurer`. You can use its API -to change one or more of the defaults to suit your needs: +This configuration is applied by `MockMvcRestDocumentationConfigurer`. +You can use its API to change one or more of the defaults to suit your needs. +The following example shows how to do so: [source,java,indent=0] ---- include::{examples-dir}/com/example/mockmvc/CustomUriConfiguration.java[tags=custom-uri-configuration] ---- -NOTE: If the port is set to the default for the configured scheme (port 80 for HTTP or -port 443 for HTTPS), it will be omitted from any URIs in the generated snippets. +NOTE: If the port is set to the default for the configured scheme (port 80 for HTTP or port 443 for HTTPS), it is omitted from any URIs in the generated snippets. + +TIP: To configure a request's context path, use the `contextPath` method on `MockHttpServletRequestBuilder`. + + -TIP: To configure a request's context path, use the `contextPath` method on -`MockHttpServletRequestBuilder`. +[[configuration-uris-rest-assured]] +==== REST Assured URI Customization + +REST Assured tests a service by making actual HTTP requests. As a result, URIs must be +customized once the operation on the service has been performed but before it is +documented. A +<> is provided for this purpose. + + + +[[configuration-uris-webtestclient]] +==== WebTestClient URI Customization + +When using WebTestClient, the default base for URIs documented by Spring REST Docs is `http://localhost:8080`. +You can customize this base by using the {spring-framework-api}/org/springframework/test/web/reactive/server/WebTestClient.Builder.html#baseUrl-java.lang.String-[ `baseUrl(String)` method on `WebTestClient.Builder`]. +The following example shows how to do so: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/webtestclient/CustomUriConfiguration.java[tags=custom-uri-configuration] +---- +<1> Configure the base of documented URIs to be `https://api.example.com`. [[configuration-snippet-encoding]] -=== Snippet encoding +=== Snippet Encoding -The default snippet encoding is `UTF-8`. You can change the default snippet encoding -using the `RestDocumentationConfigurer` API. For example, to use `ISO-8859-1`: +The default snippet encoding is `UTF-8`. +You can change the default snippet encoding by using the `RestDocumentationConfigurer` API. +For example, the following examples use `ISO-8859-1`: [source,java,indent=0,role="primary"] .MockMvc @@ -54,24 +84,31 @@ using the `RestDocumentationConfigurer` API. For example, to use `ISO-8859-1`: include::{examples-dir}/com/example/mockmvc/CustomEncoding.java[tags=custom-encoding] ---- +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/CustomEncoding.java[tags=custom-encoding] +---- + [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/CustomEncoding.java[tags=custom-encoding] ---- -TIP: When Spring REST Docs converts a request or response's content to a String, the -`charset` specified in the `Content-Type` header will be used if it is available. In its -absence, the JVM's default `Charset` will be used. The JVM's default `Charset` can be -configured using the `file.encoding` system property. +TIP: When Spring REST Docs converts the content of a request or a response to a `String`, the `charset` specified in the `Content-Type` header is used if it is available. +In its absence, the JVM's default `Charset` is used. +You can configure the JVM's default `Charset` by using the `file.encoding` system property. [[configuration-snippet-template-format]] -=== Snippet template format +=== Snippet Template Format -The default snippet template format is Asciidoctor. Markdown is also supported out of the -box. You can change the default format using the `RestDocumentationConfigurer` API: +The default snippet template format is Asciidoctor. +Markdown is also supported out of the box. +You can change the default format by using the `RestDocumentationConfigurer` API. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc @@ -79,6 +116,12 @@ box. You can change the default format using the `RestDocumentationConfigurer` A include::{examples-dir}/com/example/mockmvc/CustomFormat.java[tags=custom-format] ---- +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/CustomFormat.java[tags=custom-format] +---- + [source,java,indent=0,role="secondary"] .REST Assured ---- @@ -88,18 +131,19 @@ include::{examples-dir}/com/example/restassured/CustomFormat.java[tags=custom-fo [[configuration-default-snippets]] -=== Default snippets +=== Default Snippets -Four snippets are produced by default: +Six snippets are produced by default: -- `curl-request` -- `http-request` -- `http-response` -- `httpie-request` +* `curl-request` +* `http-request` +* `http-response` +* `httpie-request` +* `request-body` +* `response-body` -You can change the default snippet configuration during setup using the -`RestDocumentationConfigurer` API. For example, to only produce the `curl-request` -snippet by default: +You can change the default snippet configuration during setup by using the `RestDocumentationConfigurer` API. +The following examples produce only the `curl-request` snippet by default: [source,java,indent=0,role="primary"] .MockMvc @@ -107,8 +151,48 @@ snippet by default: include::{examples-dir}/com/example/mockmvc/CustomDefaultSnippets.java[tags=custom-default-snippets] ---- +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/CustomDefaultSnippets.java[tags=custom-default-snippets] +---- + [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/CustomDefaultSnippets.java[tags=custom-default-snippets] ----- \ No newline at end of file +---- + + + +[[configuration-default-preprocessors]] +=== Default Operation Preprocessors + +You can configure default request and response preprocessors during setup by using the `RestDocumentationConfigurer` API. +The following examples remove the `Foo` headers from all requests and pretty print all responses: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/CustomDefaultOperationPreprocessors.java[tags=custom-default-operation-preprocessors] +---- +<1> Apply a request preprocessor that removes the header named `Foo`. +<2> Apply a response preprocessor that pretty prints its content. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/CustomDefaultOperationPreprocessors.java[tags=custom-default-operation-preprocessors] +---- +<1> Apply a request preprocessor that removes the header named `Foo`. +<2> Apply a response preprocessor that pretty prints its content. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/CustomDefaultOperationPreprocessors.java[tags=custom-default-operation-preprocessors] +---- +<1> Apply a request preprocessor that removes the header named `Foo`. +<2> Apply a response preprocessor that pretty prints its content. + + diff --git a/docs/src/docs/asciidoc/contributing.adoc b/docs/src/docs/asciidoc/contributing.adoc index c2c594369..0093ed075 100644 --- a/docs/src/docs/asciidoc/contributing.adoc +++ b/docs/src/docs/asciidoc/contributing.adoc @@ -1,36 +1,33 @@ [[contributing]] == Contributing -Spring REST Docs is intended to make it easy for you to produce high-quality documentation -for your RESTful services. However, we can't achieve that goal without your contributions. +Spring REST Docs is intended to make it easy for you to produce high-quality documentation for your RESTful services. +However, we cannot achieve that goal without your contributions. [[contributing-questions]] === Questions -You can ask questions about Spring REST Docs on https://stackoverflow.com[StackOverflow] -using the `spring-restdocs` tag. Similarly, we encourage you to help your fellow -Spring REST Docs users by answering questions. +You can ask questions about Spring REST Docs on https://stackoverflow.com[Stack Overflow] by using the `spring-restdocs` tag. +Similarly, we encourage you to help your fellow Spring REST Docs users by answering questions. [[contributing-bugs]] === Bugs -If you believe you have found a bug, please take a moment to search the -{github}/issues?q=is%3Aissue[existing issues]. If no one else has reported the problem, -please {github}/issues/new[open a new issue] that describes the problem in detail and, -ideally, includes a test that reproduces it. +If you believe you have found a bug, please take a moment to search the {github}/issues?q=is%3Aissue[existing issues]. +If no one else has reported the problem, please {github}/issues/new[open a new issue] that describes the problem in detail and, ideally, includes a test that reproduces it. [[contributing-enhancements]] === Enhancements -If you'd like an enhancement to be made to Spring REST Docs, pull requests are most -welcome. The source code is on {github}[GitHub]. You may want to search the -{github}/issues?q=is%3Aissue[existing issues] and {github}/pulls?q=is%3Apr[pull requests] -to see if the enhancement is already being worked on. You may also want to -{github}/issues/new[open a new issue] to discuss a possible enhancement before work on it -begins. \ No newline at end of file +If you would like an enhancement to be made to Spring REST Docs, pull requests are most welcome. +The source code is on {github}[GitHub]. +You may want to search the {github}/issues?q=is%3Aissue[existing issues] and {github}/pulls?q=is%3Apr[pull requests] to see if the enhancement has already been proposed. +You may also want to {github}/issues/new[open a new issue] to discuss a possible enhancement before work on it begins. + + diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 8f0ffaf0c..b51512697 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -1,77 +1,88 @@ [[customizing-requests-and-responses]] == Customizing requests and responses -There may be situations where you do not want to document a request exactly as it was sent -or a response exactly as it was received. Spring REST Docs provides a number of -preprocessors that can be used to modify a request or response before it's documented. +There may be situations where you do not want to document a request exactly as it was sent or a response exactly as it was received. +Spring REST Docs provides a number of preprocessors that can be used to modify a request or response before it is documented. -Preprocessing is configured by calling `document` with an `OperationRequestPreprocessor`, -and/or an `OperationResponsePreprocessor`. Instances can be obtained using the -static `preprocessRequest` and `preprocessResponse` methods on `Preprocessors`. For -example: +Preprocessing is configured by calling `document` with an `OperationRequestPreprocessor` or an `OperationResponsePreprocessor`. +You can obtain instances by using the static `preprocessRequest` and `preprocessResponse` methods on `Preprocessors`. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/PerTestPreprocessing.java[tags=preprocessing] ---- -<1> Apply a request preprocessor that will remove the header named `Foo`. -<2> Apply a response preprocessor that will pretty print its content. +<1> Apply a request preprocessor that removes the header named `Foo`. +<2> Apply a response preprocessor that pretty prints its content. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/PerTestPreprocessing.java[tags=preprocessing] +---- +<1> Apply a request preprocessor that removes the header named `Foo`. +<2> Apply a response preprocessor that pretty prints its content. [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/PerTestPreprocessing.java[tags=preprocessing] ---- -<1> Apply a request preprocessor that will remove the header named `Foo`. -<2> Apply a response preprocessor that will pretty print its content. +<1> Apply a request preprocessor that removes the header named `Foo`. +<2> Apply a response preprocessor that pretty prints its content. -Alternatively, you may want to apply the same preprocessors to every test. You can do -so by configuring the preprocessors in your `@Before` method and using the -<>: +Alternatively, you may want to apply the same preprocessors to every test. +You can do so by using the `RestDocumentationConfigurer` API in your `@Before` method to configure the preprocessors. +For example, to remove the `Foo` header from all requests and pretty print all responses, you could do one of the following (depending on your testing environment): [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/EveryTestPreprocessing.java[tags=setup] ---- -<1> Create a `RestDocumentationResultHandler`, configured to preprocess the request - and response. -<2> Create a `MockMvc` instance, configured to always call the documentation result - handler. +<1> Apply a request preprocessor that removes the header named `Foo`. +<2> Apply a response preprocessor that pretty prints its content. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/EveryTestPreprocessing.java[tags=setup] +---- +<1> Apply a request preprocessor that removes the header named `Foo`. +<2> Apply a response preprocessor that pretty prints its content. [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/EveryTestPreprocessing.java[tags=setup] ---- -<1> Create a `RestDocumentationFilter`, configured to preprocess the request - and response. -<2> Create a `RequestSpecification` instance, configured to always call the documentation - filter. +<1> Apply a request preprocessor that removes the header named `Foo`. +<2> Apply a response preprocessor that pretty prints its content. -Then, in each test, any configuration specific to that test can be performed. For example: +Then, in each test, you can perform any configuration specific to that test. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/EveryTestPreprocessing.java[tags=use] ---- -<1> The request and response will be preprocessed due to the use of `alwaysDo` above. -<2> Document the links specific to the resource that is being tested + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/EveryTestPreprocessing.java[tags=use] +---- [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/EveryTestPreprocessing.java[tags=use] ---- -<1> The request and response will be preprocessed due to the configuration of the -`RequestSpecification` in the `setUp` method. -<2> Document the links specific to the resource that is being tested -Various built in preprocessors, including those illustrated above, are available via the -static methods on `Preprocessors`. See <> for further details. +Various built-in preprocessors, including those illustrated above, are available through the static methods on `Preprocessors`. +See <> for further details. @@ -81,71 +92,53 @@ static methods on `Preprocessors`. See <> for further deta [[customizing-requests-and-responses-preprocessors-pretty-print]] -==== Pretty printing +==== Pretty Printing -`prettyPrint` on `Preprocessors` formats the content of the request or response -to make it easier to read. +`prettyPrint` on `Preprocessors` formats the content of the request or response to make it easier to read. [[customizing-requests-and-responses-preprocessors-mask-links]] -==== Masking links - -If you're documenting a Hypermedia-based API, you may want to encourage clients to -navigate the API using links rather than through the use of hard coded URIs. One way to do -this is to limit the use of URIs in the documentation. `maskLinks` on -`Preprocessors` replaces the `href` of any links in the response with `...`. A -different replacement can also be specified if you wish. +==== Masking Links +If you are documenting a hypermedia-based API, you may want to encourage clients to navigate the API by using links rather than through the use of hard coded URIs. +One way to do so is to limit the use of URIs in the documentation. +`maskLinks` on `Preprocessors` replaces the `href` of any links in the response with `...`. +You can also specify a different replacement if you wish. -[[customizing-requests-and-responses-preprocessors-remove-headers]] -==== Removing headers -`removeHeaders` on `Preprocessors` removes any headers from the request or response where -the name is equal to any of the given header names. +[[customizing-requests-and-responses-preprocessors-modify-headers]] +==== Modifying Headers -`removeMatchingHeaders` on `Preprocessors` removes any headers from the request or -response where the name matches any of the given regular expression patterns. +You can use `modifyHeaders` on `Preprocessors` to add, set, and remove request or response headers. [[customizing-requests-and-responses-preprocessors-replace-patterns]] -==== Replacing patterns - -`replacePattern` on `Preprocessors` provides a general purpose mechanism for -replacing content in a request or response. Any occurrences of a regular expression are -replaced. - +==== Replacing Patterns - -[[customizing-requests-and-responses-preprocessors-modify-request-parameters]] -==== Modifying request parameters - -`modifyParameters` on `Preprocessors` can be used to add, set, and remove request -parameters. +`replacePattern` on `Preprocessors` provides a general purpose mechanism for replacing content in a request or response. +Any occurrences that match a regular expression are replaced. [[customizing-requests-and-responses-preprocessors-modify-uris]] ==== Modifying URIs -TIP: If you are using MockMvc, URIs should be customized by <>. +TIP: If you use MockMvc or a WebTestClient that is not bound to a server, you should customize URIs by <>. -`modifyUris` on `RestAssuredPreprocessors` can be used to modify any URIs in a request -or a response. When using REST Assured, this allows you to customize the URIs that appear -in the documentation while testing a local instance of the service. +You can use `modifyUris` on `Preprocessors` to modify any URIs in a request or a response. +When using REST Assured or WebTestClient bound to a server, this lets you customize the URIs that appear in the documentation while testing a local instance of the service. [[customizing-requests-and-responses-preprocessors-writing-your-own]] -==== Writing your own preprocessor +==== Writing Your Own Preprocessor + +If one of the built-in preprocessors does not meet your needs, you can write your own by implementing the `OperationPreprocessor` interface. +You can then use your custom preprocessor in exactly the same way as any of the built-in preprocessors. + +If you want to modify only the content (body) of a request or response, consider implementing the `ContentModifier` interface and using it with the built-in `ContentModifyingOperationPreprocessor`. -If one of the built-in preprocessors does not meet your needs, you can write your own by -implementing the `OperationPreprocessor` interface. You can then use your custom -preprocessor in exactly the same way as any of the built-in preprocessors. -If you only want to modify the content (body) of a request or response, consider -implementing the `ContentModifier` interface and using it with the built-in -`ContentModifyingOperationPreprocessor`. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index d4918e4b6..cb9ce82a3 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -8,8 +8,8 @@ This section provides more details about using Spring REST Docs to document your [[documenting-your-api-hypermedia]] === Hypermedia -Spring REST Docs provides support for documenting the links in a -https://en.wikipedia.org/wiki/HATEOAS[Hypermedia-based] API: +Spring REST Docs provides support for documenting the links in a https://en.wikipedia.org/wiki/HATEOAS[hypermedia-based] API. +The following examples show how to use it: [source,java,indent=0,role="primary"] .MockMvc @@ -17,11 +17,21 @@ https://en.wikipedia.org/wiki/HATEOAS[Hypermedia-based] API: include::{examples-dir}/com/example/mockmvc/Hypermedia.java[tag=links] ---- <1> Configure Spring REST docs to produce a snippet describing the response's links. - Uses the static `links` method on - `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. -<2> Expect a link whose rel is `alpha`. Uses the static `linkWithRel` method on - `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. -<3> Expect a link whose rel is `bravo`. +Uses the static `links` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +<2> Expect a link whose `rel` is `alpha`. +Uses the static `linkWithRel` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +<3> Expect a link whose `rel` is `bravo`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/Hypermedia.java[tag=links] +---- +<1> Configure Spring REST docs to produce a snippet describing the response's links. +Uses the static `links` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +<2> Expect a link whose `rel` is `alpha`. +Uses the static `linkWithRel` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +<3> Expect a link whose `rel` is `bravo`. [source,java,indent=0,role="secondary"] .REST Assured @@ -31,51 +41,55 @@ include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=links] <1> Configure Spring REST docs to produce a snippet describing the response's links. Uses the static `links` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. -<2> Expect a link whose rel is `alpha`. Uses the static `linkWithRel` method on +<2> Expect a link whose `rel` is `alpha`. Uses the static `linkWithRel` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. -<3> Expect a link whose rel is `bravo`. +<3> Expect a link whose `rel` is `bravo`. -The result is a snippet named `links.adoc` that contains a table describing the resource's -links. +The result is a snippet named `links.adoc` that contains a table describing the resource's links. -TIP: If a link in the response has a `title`, the description can be omitted from its -descriptor and the `title` will be used. If you omit the description and the link does -not have a `title` a failure will occur. +TIP: If a link in the response has a `title`, you can omit the description from its descriptor and the `title` is used. +If you omit the description and the link does not have a `title`, a failure occurs. -When documenting links, the test will fail if an undocumented link is found in the -response. Similarly, the test will also fail if a documented link is not found in the -response and the link has not been marked as optional. +When documenting links, the test fails if an undocumented link is found in the response. +Similarly, the test also fails if a documented link is not found in the response and the link has not been marked as optional. -If you do not want to document a link, you can mark it as ignored. This will prevent it -from appearing in the generated snippet while avoiding the failure described above. +If you do not want to document a link, you can mark it as ignored. +Doing so prevents it from appearing in the generated snippet while avoiding the failure described above. -Links can also be documented in a relaxed mode where any undocumented links will not cause -a test failure. To do so, use the `relaxedLinks` method on -`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. This can be useful when -documenting a particular scenario where you only want to focus on a subset of the links. +You can also document links in a relaxed mode, where any undocumented links do not cause a test failure. +To do so, use the `relaxedLinks` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +This can be useful when documenting a particular scenario where you only want to focus on a subset of the links. [[documenting-your-api-hypermedia-link-formats]] -==== Hypermedia link formats +==== Hypermedia Link Formats Two link formats are understood by default: - * Atom – links are expected to be in an array named `links`. Used by default when the - content type of the response is compatible with `application/json`. - * HAL – links are expected to be in a map named `_links`. Used by default when the - content type of the response is compatible with `application/hal+json`. +* Atom: Links are expected to be in an array named `links`. + This is used by default when the content type of the response is compatible with `application/json`. +* HAL: Links are expected to be in a map named `_links`. + This is used by default when the content type of the response is compatible with `application/hal+json`. -If you are using Atom or HAL-format links but with a different content type you can -provide one of the built-in `LinkExtractor` implementations to `links`. For example: +If you use Atom- or HAL-format links but with a different content type, you can provide one of the built-in `LinkExtractor` implementations to `links`. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/Hypermedia.java[tag=explicit-extractor] ---- -<1> Indicate that the links are in HAL format. Uses the static `halLinks` method on -`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +<1> Indicate that the links are in HAL format. +Uses the static `halLinks` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/Hypermedia.java[tag=explicit-extractor] +---- +<1> Indicate that the links are in HAL format. +Uses the static `halLinks` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. [source,java,indent=0,role="secondary"] .REST Assured @@ -85,94 +99,159 @@ include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=explicit-ext <1> Indicate that the links are in HAL format. Uses the static `halLinks` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. -If your API represents its links in a format other than Atom or HAL, you can provide your -own implementation of the `LinkExtractor` interface to extract the links from the -response. +If your API represents its links in a format other than Atom or HAL, you can provide your own implementation of the `LinkExtractor` interface to extract the links from the response. [[documenting-your-api-hypermedia-ignoring-common-links]] -==== Ignoring common links +==== Ignoring Common Links -Rather than documenting links that are common to every response, such as `_self` and -`curies` when using HAL, you may want to document them once in an overview section and -then ignore them in the rest of your API's documentation. To do so, you can build on the -<> to add link -descriptors to a snippet that's preconfigured to ignore certain links. For example: +Rather than documenting links that are common to every response, such as `self` and `curies` when using HAL, you may want to document them once in an overview section and then ignore them in the rest of your API's documentation. +To do so, you can build on the <> to add link descriptors to a snippet that is preconfigured to ignore certain links. +The following example shows how to do so: [source,java,indent=0] ---- include::{examples-dir}/com/example/Hypermedia.java[tags=ignore-links] ---- + + [[documenting-your-api-request-response-payloads]] -=== Request and response payloads +=== Request and Response Payloads + +In addition to the hypermedia-specific support <>, support for general documentation of request and response payloads is also provided. -In addition to the hypermedia-specific support <>, support for general documentation of request and response payloads is also -provided. For example: +By default, Spring REST Docs automatically generates snippets for the body of the request and the body of the response. +These snippets are named `request-body.adoc` and `response-body.adoc` respectively. + + + +[[documenting-your-api-request-response-payloads-fields]] +==== Request and Response Fields + +To provide more detailed documentation of a request or response payload, support for documenting the payload's fields is provided. + +Consider the following payload: + +[source,json,indent=0] +---- + { + "contact": { + "name": "Jane Doe", + "email": "jane.doe@example.com" + } + } +---- + +You can document the previous example's fields as follows: [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/Payload.java[tags=response] ---- -<1> Configure Spring REST docs to produce a snippet describing the fields in the response - payload. To document a request `requestFields` can be used. Both are static methods on - `org.springframework.restdocs.payload.PayloadDocumentation`. -<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on - `org.springframework.restdocs.payload.PayloadDocumentation`. -<3> Expect a field with the path `contact.email`. +<1> Configure Spring REST docs to produce a snippet describing the fields in the response payload. +To document a request, you can use `requestFields`. +Both are static methods on `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Expect a field with the path `contact.email`. +Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. +<3> Expect a field with the path `contact.name`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/Payload.java[tags=response] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the response payload. +To document a request, you can use `requestFields`. +Both are static methods on `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Expect a field with the path `contact.email`. +Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. +<3> Expect a field with the path `contact.name`. [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/Payload.java[tags=response] ---- -<1> Configure Spring REST docs to produce a snippet describing the fields in the response - payload. To document a request `requestFields` can be used. Both are static methods on - `org.springframework.restdocs.payload.PayloadDocumentation`. -<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on - `org.springframework.restdocs.payload.PayloadDocumentation`. -<3> Expect a field with the path `contact.email`. +<1> Configure Spring REST docs to produce a snippet describing the fields in the response payload. +To document a request, you can use `requestFields`. +Both are static methods on `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Expect a field with the path `contact.email`. +Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. +<3> Expect a field with the path `contact.name`. + +The result is a snippet that contains a table describing the fields. +For requests, this snippet is named `request-fields.adoc`. +For responses, this snippet is named `response-fields.adoc`. -The result is a snippet that contains a table describing the fields. For requests this -snippet is named `request-fields.adoc`. For responses this snippet is named -`response-fields.adoc`. +When documenting fields, the test fails if an undocumented field is found in the payload. +Similarly, the test also fails if a documented field is not found in the payload and the field has not been marked as optional. -When documenting fields, the test will fail if an undocumented field is found in the -payload. Similarly, the test will also fail if a documented field is not found in the -payload and the field has not been marked as optional. For payloads with a hierarchical -structure, documenting a field is sufficient for all of its descendants to also be -treated as having been documented. +If you do not want to provide detailed documentation for all of the fields, an entire subsection of a payload can be documented. +The following examples show how to do so: -If you do not want to document a field, you can mark it as ignored. This will prevent it -from appearing in the generated snippet while avoiding the failure described above. +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection] +---- +<1> Document the subsection with the path `contact`. +`contact.email` and `contact.name` are now seen as having also been documented. +Uses the static `subsectionWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. -Fields can also be documented in a relaxed mode where any undocumented fields will not -cause a test failure. To do so, use the `relaxedRequestFields` and `relaxedResponseFields` -methods on -`org.springframework.restdocs.payload.PayloadDocumentation`. This can be useful when -documenting a particular scenario where you only want to focus on a subset of the payload. +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/Payload.java[tags=subsection] +---- +<1> Document the subsection with the path `contact`. +`contact.email` and `contact.name` are now seen as having also been documented. +Uses the static `subsectionWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection] +---- +<1> Document the subsection with the path `contact`. `contact.email` and `contact.name` are now seen as having also been documented. +Uses the static `subsectionWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. -TIP: By default, Spring REST Docs will assume that the payload you are documenting is -JSON. If you want to document an XML payload the content type of the request or response -must be compatible with `application/xml`. +`subsectionWithPath` can be useful for providing a high-level overview of a particular section of a payload. +You can then produce separate, more detailed documentation for a subsection. +See <>. -[[documenting-your-api-request-response-payloads-json]] -==== JSON payloads +If you do not want to document a field or subsection at all, you can mark it as ignored. +This prevents it from appearing in the generated snippet while avoiding the failure described earlier. -[[documenting-your-api-request-response-payloads-json-field-paths]] -===== JSON field paths +You can also document fields in a relaxed mode, where any undocumented fields do not cause a test failure. +To do so, use the `relaxedRequestFields` and `relaxedResponseFields` methods on `org.springframework.restdocs.payload.PayloadDocumentation`. +This can be useful when documenting a particular scenario where you want to focus only on a subset of the payload. -JSON field paths use either dot notation or bracket notation. Dot notation uses '.' to -separate each key in the path; `a.b`, for example. Bracket notation wraps each key in -square brackets and single quotes; `['a']['b']`, for example. In either case, `[]` is used -to identify an array. Dot notation is more concise, but using bracket notation enables the -use of `.` within a key name; `['a.b']`, for example. The two different notations can be -used in the same path; `a['b']`, for example. +TIP: By default, Spring REST Docs assumes that the payload you are documenting is JSON. +If you want to document an XML payload, the content type of the request or response must be compatible with `application/xml`. -With this JSON payload: + + +[[documenting-your-api-request-response-payloads-fields-json]] +===== Fields in JSON Payloads + +This section describes how to work with fields in JSON payloads. + + + +[[documenting-your-api-request-response-payloads-fields-json-field-paths]] +====== JSON Field Paths + +JSON field paths use either dot notation or bracket notation. +Dot notation uses '.' to separate each key in the path (for example, `a.b`). +Bracket notation wraps each key in square brackets and single quotation marks (for example, `['a']['b']`). +In either case, `[]` is used to identify an array. +Dot notation is more concise, but using bracket notation enables the use of `.` within a key name (for example, `['a.b']`). +The two different notations can be used in the same path (for example, `a['b']`). + +Consider the following JSON payload: [source,json,indent=0] ---- @@ -194,7 +273,7 @@ With this JSON payload: } ---- -The following paths are all present: +In the preceding JSON payload, the following paths are all present: [cols="1,3"] |=== @@ -229,14 +308,12 @@ The following paths are all present: |`['a']['e.dot']` |The string `four` - - |=== -A response that uses an array at its root can also be documented. The path `[]` will refer -to the entire array. You can then use bracket or dot notation to identify fields within -the array's entries. For example, `[].id` corresponds to the `id` field of every object -found in the following array: +You can also document a payload that uses an array at its root. +The path `[]` refers to the entire array. +You can then use bracket or dot notation to identify fields within the array's entries. +For example, `[].id` corresponds to the `id` field of every object found in the following array: [source,json,indent=0] ---- @@ -250,86 +327,113 @@ found in the following array: ] ---- +You can use `\*` as a wildcard to match fields with different names. +For example, `users.*.role` could be used to document the role of every user in the following JSON: + +[source,json,indent=0] +---- + { + "users":{ + "ab12cd34":{ + "role": "Administrator" + }, + "12ab34cd":{ + "role": "Guest" + } + } + } +---- + -[[documenting-your-api-request-response-payloads-json-field-types]] -===== JSON field types +[[documenting-your-api-request-response-payloads-fields-json-field-types]] +====== JSON Field Types -When a field is documented, Spring REST Docs will attempt to determine its type by -examining the payload. Seven different types are supported: +When a field is documented, Spring REST Docs tries to determine its type by examining the payload. +Seven different types are supported: [cols="1,3"] |=== | Type | Description -| array -| The value of each occurrence of the field is an array +| `array` +| The value of each occurrence of the field is an array. -| boolean -| The value of each occurrence of the field is a boolean (`true` or `false`) +| `boolean` +| The value of each occurrence of the field is a boolean (`true` or `false`). -| object -| The value of each occurrence of the field is an object +| `object` +| The value of each occurrence of the field is an object. -| number -| The value of each occurrence of the field is a number +| `number` +| The value of each occurrence of the field is a number. -| null -| The value of each occurrence of the field is `null` +| `null` +| The value of each occurrence of the field is `null`. -| string -| The value of each occurrence of the field is a string +| `string` +| The value of each occurrence of the field is a string. -| varies -| The field occurs multiple times in the payload with a variety of different types +| `varies` +| The field occurs multiple times in the payload with a variety of different types. |=== -The type can also be set explicitly using the `type(Object)` method on -`FieldDescriptor`. The result of the supplied ``Object``'s `toString` method will be used -in the documentation. Typically, one of the values enumerated by `JsonFieldType` will be -used: +You can also explicitly set the type by using the `type(Object)` method on `FieldDescriptor`. +The result of the `toString` method of the supplied `Object` is used in the documentation. +Typically, one of the values enumerated by `JsonFieldType` is used. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/Payload.java[tags=explicit-type] ---- -<1> Set the field's type to `string`. +<1> Set the field's type to `String`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/Payload.java[tags=explicit-type] +---- +<1> Set the field's type to `String`. [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/Payload.java[tags=explicit-type] ---- -<1> Set the field's type to `string`. +<1> Set the field's type to `String`. + + + +[[documenting-your-api-request-response-payloads-fields-xml]] +===== XML payloads +This section describes how to work with XML payloads. -[[documenting-your-api-request-response-payloads-xml]] -==== XML payloads -[[documenting-your-api-request-response-payloads-xml-field-paths]] -===== XML field paths -XML field paths are described using XPath. `/` is used to descend into a child node. +[[documenting-your-api-request-response-payloads-fields-xml-field-paths]] +====== XML Field Paths +XML field paths are described using XPath. +`/` is used to descend into a child node. -[[documenting-your-api-request-response-payloads-xml-field-types]] -===== XML field types -When documenting an XML payload, you must provide a type for the field using the -`type(Object)` method on `FieldDescriptor`. The result of the supplied type's `toString` -method will be used in the documentation. +[[documenting-your-api-request-response-payloads-fields-xml-field-types]] +====== XML Field Types +When documenting an XML payload, you must provide a type for the field by using the `type(Object)` method on `FieldDescriptor`. +The result of the supplied type's `toString` method is used in the documentation. -[[documenting-your-api-request-response-payloads-reusing-field-descriptors]] -==== Reusing field descriptors -In addition to the general support for <>, the request and response snippets allow additional descriptors to be -configured with a path prefix. This allows the descriptors for a repeated portion of a -request or response payload to be created once and then reused. +[[documenting-your-api-request-response-payloads-fields-reusing-field-descriptors]] +===== Reusing Field Descriptors + +In addition to the general support for <>, the request and response snippets let additional descriptors be configured with a path prefix. +This lets the descriptors for a repeated portion of a request or response payload be created once and then reused. Consider an endpoint that returns a book: @@ -341,7 +445,7 @@ Consider an endpoint that returns a book: } ---- -The paths for `title` and `author` are simply `title` and `author` respectively. +The paths for `title` and `author` are `title` and `author`, respectively. Now consider an endpoint that returns an array of books: @@ -357,126 +461,308 @@ Now consider an endpoint that returns an array of books: }] ---- -The paths for `title` and `author` are `[].title` and `[].author` respectively. The only -difference between the single book and the array of books is that the fields' paths now -have a `[].` prefix. +The paths for `title` and `author` are `[].title` and `[].author`, respectively. +The only difference between the single book and the array of books is that the fields' paths now have a `[].` prefix. -The descriptors that document a book can be created: +You can create the descriptors that document a book as follows: [source,java,indent=0] ---- include::{examples-dir}/com/example/Payload.java[tags=book-descriptors] ---- -They can then be used to document a single book: +You can then use them to document a single book, as follows: [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/Payload.java[tags=single-book] ---- -<1> Document `title` and `author` using existing descriptors +<1> Document `title` and `author` by using existing descriptors + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/Payload.java[tags=single-book] +---- +<1> Document `title` and `author` by using existing descriptors [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/Payload.java[tags=single-book] ---- -<1> Document `title` and `author` using existing descriptors +<1> Document `title` and `author` by using existing descriptors -And an array of books: +You can also use the descriptors to document an array of books, as follows: [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/Payload.java[tags=book-array] ---- -<1> Document the array -<2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` +<1> Document the array. +<2> Document `[].title` and `[].author` by using the existing descriptors prefixed with `[].` + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/Payload.java[tags=book-array] +---- +<1> Document the array. +<2> Document `[].title` and `[].author` by using the existing descriptors prefixed with `[].` + + [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array] ---- -<1> Document the array -<2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` +<1> Document the array. +<2> Document `[].title` and `[].author` by using the existing descriptors prefixed with `[].` + + + +[[documenting-your-api-request-response-payloads-subsections]] +==== Documenting a Subsection of a Request or Response Payload + +If a payload is large or structurally complex, it can be useful to document individual sections of the payload. +REST Docs let you do so by extracting a subsection of the payload and then documenting it. + + + +[[documenting-your-api-request-response-payloads-subsections-body]] +===== Documenting a Subsection of a Request or Response Body + +Consider the following JSON response body: + +[source,json,indent=0] +---- + { + "weather": { + "wind": { + "speed": 15.3, + "direction": 287.0 + }, + "temperature": { + "high": 21.2, + "low": 14.8 + } + } + } +---- + +You can produce a snippet that documents the `temperature` object as follows: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=body-subsection] +---- +<1> Produce a snippet containing a subsection of the response body. +Uses the static `responseBody` and `beneathPath` methods on `org.springframework.restdocs.payload.PayloadDocumentation`. +To produce a snippet for the request body, you can use `requestBody` in place of `responseBody`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/Payload.java[tags=body-subsection] +---- +<1> Produce a snippet containing a subsection of the response body. +Uses the static `responseBody` and `beneathPath` methods on `org.springframework.restdocs.payload.PayloadDocumentation`. +To produce a snippet for the request body, you can use `requestBody` in place of `responseBody`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=body-subsection] +---- +<1> Produce a snippet containing a subsection of the response body. +Uses the static `responseBody` and `beneathPath` methods on `org.springframework.restdocs.payload.PayloadDocumentation`. +To produce a snippet for the request body, you can use `requestBody` in place of `responseBody`. + +The result is a snippet with the following contents: + +[source,json,indent=0] +---- + { + "temperature": { + "high": 21.2, + "low": 14.8 + } + } +---- + +To make the snippet's name distinct, an identifier for the subsection is included. +By default, this identifier is `beneath-${path}`. +For example, the preceding code results in a snippet named `response-body-beneath-weather.temperature.adoc`. +You can customize the identifier by using the `withSubsectionId(String)` method, as follows: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id] +---- + +The result is a snippet named `request-body-temp.adoc`. + + + +[[documenting-your-api-request-response-payloads-subsections-fields]] +===== Documenting the Fields of a Subsection of a Request or Response + +As well as documenting a subsection of a request or response body, you can also document the fields in a particular subsection. +You can produce a snippet that documents the fields of the `temperature` object (`high` and `low`) as follows: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=fields-subsection] +---- +<1> Produce a snippet describing the fields in the subsection of the response payload beneath the path `weather.temperature`. +Uses the static `beneathPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Document the `high` and `low` fields. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/Payload.java[tags=fields-subsection] +---- +<1> Produce a snippet describing the fields in the subsection of the response payload beneath the path `weather.temperature`. +Uses the static `beneathPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Document the `high` and `low` fields. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=fields-subsection] +---- +<1> Produce a snippet describing the fields in the subsection of the response payload + beneath the path `weather.temperature`. Uses the static `beneathPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Document the `high` and `low` fields. + +The result is a snippet that contains a table describing the `high` and `low` fields of `weather.temperature`. +To make the snippet's name distinct, an identifier for the subsection is included. +By default, this identifier is `beneath-${path}`. +For example, the preceding code results in a snippet named `response-fields-beneath-weather.temperature.adoc`. -[[documenting-your-api-request-parameters]] -=== Request parameters -A request's parameters can be documented using `requestParameters`. Request parameters -can be included in a `GET` request's query string. For example: + +[[documenting-your-api-query-parameters]] +=== Query Parameters + +You can document a request's query parameters by using `queryParameters`. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- -include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request-parameters-query-string] +include::{examples-dir}/com/example/mockmvc/QueryParameters.java[tags=query-parameters] +---- +<1> Perform a `GET` request with two parameters, `page` and `per_page`, in the query string. +Query parameters should be included in the URL, as shown here, or specified using the request builder's `queryParam` or `queryParams` method. +The `param` and `params` methods should be avoided as the source of the parameters is then ambiguous. +<2> Configure Spring REST Docs to produce a snippet describing the request's query parameters. +Uses the static `queryParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the `page` query parameter. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. +<4> Document the `per_page` query parameter. + +[source,java,indent=0,role="secondary"] +.WebTestClient ---- -<1> Perform a `GET` request with two parameters, `page` and `per_page` in the query - string. -<2> Configure Spring REST Docs to produce a snippet describing the request's parameters. - Uses the static `requestParameters` method on - `org.springframework.restdocs.request.RequestDocumentation`. -<3> Document the `page` parameter. Uses the static `parameterWithName` method on - `org.springframework.restdocs.request.RequestDocumentation`. +include::{examples-dir}/com/example/webtestclient/QueryParameters.java[tags=query-parameters] +---- +<1> Perform a `GET` request with two parameters, `page` and `per_page`, in the query string. +<2> Configure Spring REST Docs to produce a snippet describing the request's query parameters. +Uses the static `queryParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the `page` parameter. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the `per_page` parameter. [source,java,indent=0,role="secondary"] .REST Assured ---- -include::{examples-dir}/com/example/restassured/RequestParameters.java[tags=request-parameters-query-string] +include::{examples-dir}/com/example/restassured/QueryParameters.java[tags=query-parameters] ---- -<1> Configure Spring REST Docs to produce a snippet describing the request's parameters. - Uses the static `requestParameters` method on - `org.springframework.restdocs.request.RequestDocumentation`. -<2> Document the `page` parameter. Uses the static `parameterWithName` method on - `org.springframework.restdocs.request.RequestDocumentation`. +<1> Configure Spring REST Docs to produce a snippet describing the request's query parameters. +Uses the static `queryParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the `page` parameter. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. <3> Document the `per_page` parameter. -<4> Perform a `GET` request with two parameters, `page` and `per_page` in the query - string. +<4> Perform a `GET` request with two parameters, `page` and `per_page`, in the query string. + +When documenting query parameters, the test fails if an undocumented query parameter is used in the request's query string. +Similarly, the test also fails if a documented query parameter is not found in the request's query string and the parameter has not been marked as optional. + +If you do not want to document a query parameter, you can mark it as ignored. +This prevents it from appearing in the generated snippet while avoiding the failure described above. + +You can also document query parameters in a relaxed mode where any undocumented parameters do not cause a test failure. +To do so, use the `relaxedQueryParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +This can be useful when documenting a particular scenario where you only want to focus on a subset of the query parameters. -Request parameters can also be included as form data in the body of a POST request: + + +[[documenting-your-api-form-parameters]] +=== Form Parameters + +You can document a request's form parameters by using `formParameters`. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- -include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request-parameters-form-data] +include::{examples-dir}/com/example/mockmvc/FormParameters.java[tags=form-parameters] +---- +<1> Perform a `POST` request with a single form parameter, `username`. +<2> Configure Spring REST Docs to produce a snippet describing the request's form parameters. +Uses the static `formParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the `username` parameter. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. + +[source,java,indent=0,role="secondary"] +.WebTestClient ---- -<1> Perform a `POST` request with a single parameter, `username`. +include::{examples-dir}/com/example/webtestclient/FormParameters.java[tags=form-parameters] +---- +<1> Perform a `POST` request with a single form parameter, `username`. +<2> Configure Spring REST Docs to produce a snippet describing the request's form parameters. +Uses the static `formParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the `username` parameter. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. [source,java,indent=0,role="secondary"] .REST Assured ---- -include::{examples-dir}/com/example/restassured/RequestParameters.java[tags=request-parameters-form-data] +include::{examples-dir}/com/example/restassured/FormParameters.java[tags=form-parameters] ---- -<1> Configure the `username` parameter. -<2> Perform the `POST` request. +<1> Configure Spring REST Docs to produce a snippet describing the request's form parameters. +Uses the static `formParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the `username` parameter. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Perform a `POST` request with a single form parameter, `username`. -In both cases, the result is a snippet named `request-parameters.adoc` that contains a -table describing the parameters that are supported by the resource. +In all cases, the result is a snippet named `form-parameters.adoc` that contains a table describing the form parameters that are supported by the resource. -When documenting request parameters, the test will fail if an undocumented request -parameter is used in the request. Similarly, the test will also fail if a documented -request parameter is not found in the request and the request parameter has not been -marked as optional. +When documenting form parameters, the test fails if an undocumented form parameter is used in the request body. +Similarly, the test also fails if a documented form parameter is not found in the request body and the form parameter has not been marked as optional. -If you do not want to document a request parameter, you can mark it as ignored. This will -prevent it from appearing in the generated snippet while avoiding the failure described -above. +If you do not want to document a form parameter, you can mark it as ignored. +This prevents it from appearing in the generated snippet while avoiding the failure described above. -Request parameters can also be documented in a relaxed mode where any undocumented -parameters will not cause a test failure. To do so, use the `relaxedRequestParameters` -method on `org.springframework.restdocs.request.RequestDocumentation`. This can be useful -when documenting a particular scenario where you only want to focus on a subset of the -request parameters. +You can also document form parameters in a relaxed mode where any undocumented parameters do not cause a test failure. +To do so, use the `relaxedFormParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +This can be useful when documenting a particular scenario where you only want to focus on a subset of the form parameters. [[documenting-your-api-path-parameters]] -=== Path parameters +=== Path Parameters -A request's path parameters can be documented using `pathParameters`. For example: +You can document a request's path parameters by using `pathParameters`. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc @@ -484,11 +770,22 @@ A request's path parameters can be documented using `pathParameters`. For exampl include::{examples-dir}/com/example/mockmvc/PathParameters.java[tags=path-parameters] ---- <1> Perform a `GET` request with two path parameters, `latitude` and `longitude`. -<2> Configure Spring REST Docs to produce a snippet describing the request's path - parameters. Uses the static `pathParameters` method on - `org.springframework.restdocs.request.RequestDocumentation`. -<3> Document the parameter named `latitude`. Uses the static `parameterWithName` method on - `org.springframework.restdocs.request.RequestDocumentation`. +<2> Configure Spring REST Docs to produce a snippet describing the request's path parameters. +Uses the static `pathParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the parameter named `latitude`. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. +<4> Document the parameter named `longitude`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/PathParameters.java[tags=path-parameters] +---- +<1> Perform a `GET` request with two path parameters, `latitude` and `longitude`. +<2> Configure Spring REST Docs to produce a snippet describing the request's path parameters. +Uses the static `pathParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the parameter named `latitude`. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the parameter named `longitude`. [source,java,indent=0,role="secondary"] @@ -496,41 +793,32 @@ include::{examples-dir}/com/example/mockmvc/PathParameters.java[tags=path-parame ---- include::{examples-dir}/com/example/restassured/PathParameters.java[tags=path-parameters] ---- -<1> Configure Spring REST Docs to produce a snippet describing the request's path - parameters. Uses the static `pathParameters` method on - `org.springframework.restdocs.request.RequestDocumentation`. -<2> Document the parameter named `latitude`. Uses the static `parameterWithName` method on - `org.springframework.restdocs.request.RequestDocumentation`. +<1> Configure Spring REST Docs to produce a snippet describing the request's path parameters. +Uses the static `pathParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the parameter named `latitude`. +Uses the static `parameterWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. <3> Document the parameter named `longitude`. <4> Perform a `GET` request with two path parameters, `latitude` and `longitude`. -The result is a snippet named `path-parameters.adoc` that contains a table describing -the path parameters that are supported by the resource. +The result is a snippet named `path-parameters.adoc` that contains a table describing the path parameters that are supported by the resource. -TIP: To make the path parameters available for documentation, the request must be -built using one of the methods on `RestDocumentationRequestBuilders` rather than -`MockMvcRequestBuilders`. +When documenting path parameters, the test fails if an undocumented path parameter is used in the request. +Similarly, the test also fails if a documented path parameter is not found in the request and the path parameter has not been marked as optional. -When documenting path parameters, the test will fail if an undocumented path parameter -is used in the request. Similarly, the test will also fail if a documented path parameter -is not found in the request and the path parameter has not been marked as optional. +You can also document path parameters in a relaxed mode, where any undocumented parameters do not cause a test failure. +To do so, use the `relaxedPathParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +This can be useful when documenting a particular scenario where you only want to focus on a subset of the path parameters. -Path parameters can also be documented in a relaxed mode where any undocumented -parameters will not cause a test failure. To do so, use the `relaxedPathParameters` -method on `org.springframework.restdocs.request.RequestDocumentation`. This can be useful -when documenting a particular scenario where you only want to focus on a subset of the -path parameters. - -If you do not want to document a path parameter, you can mark it as ignored. This will -prevent it from appearing in the generated snippet while avoiding the failure described -above. +If you do not want to document a path parameter, you can mark it as ignored. +Doing so prevents it from appearing in the generated snippet while avoiding the failure described earlier. [[documenting-your-api-request-parts]] -=== Request parts +=== Request Parts -The parts of a multipart request can be documenting using `requestParts`. For example: +You can use `requestParts` to document the parts of a multipart request. +The following example shows how to do so: [source,java,indent=0,role="primary"] .MockMvc @@ -538,63 +826,173 @@ The parts of a multipart request can be documenting using `requestParts`. For ex include::{examples-dir}/com/example/mockmvc/RequestParts.java[tags=request-parts] ---- <1> Perform a `POST` request with a single part named `file`. -<2> Configure Spring REST Docs to produce a snippet describing the request's parts. Uses - the static `requestParts` method on - `org.springframework.restdocs.request.RequestDocumentation`. -<3> Document the part named `file`. Uses the static `partWithName` method on - `org.springframework.restdocs.request.RequestDocumentation`. +<2> Configure Spring REST Docs to produce a snippet describing the request's parts. +Uses the static `requestParts` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the part named `file`. +Uses the static `partWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/RequestParts.java[tags=request-parts] +---- +<1> Perform a `POST` request with a single part named `file`. +<2> Configure Spring REST Docs to produce a snippet describing the request's parts. +Uses the static `requestParts` method on `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the part named `file`. +Uses the static `partWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/RequestParts.java[tags=request-parts] ---- -<1> Configure Spring REST Docs to produce a snippet describing the request's parts. Uses - the static `requestParts` method on - `org.springframework.restdocs.request.RequestDocumentation`. -<2> Document the part named `file`. Uses the static `partWithName` method on - `org.springframework.restdocs.request.RequestDocumentation`. +<1> Configure Spring REST Docs to produce a snippet describing the request's parts. +Uses the static `requestParts` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the part named `file`. Uses the static `partWithName` method on `org.springframework.restdocs.request.RequestDocumentation`. <3> Configure the request with the part named `file`. <4> Perform the `POST` request to `/upload`. -The result is a snippet named `request-parts.adoc` that contains a table describing the -request parts that are supported by the resource. +The result is a snippet named `request-parts.adoc` that contains a table describing the request parts that are supported by the resource. + +When documenting request parts, the test fails if an undocumented part is used in the request. +Similarly, the test also fails if a documented part is not found in the request and the part has not been marked as optional. + +You can also document request parts in a relaxed mode where any undocumented parts do not cause a test failure. +To do so, use the `relaxedRequestParts` method on `org.springframework.restdocs.request.RequestDocumentation`. +This can be useful when documenting a particular scenario where you only want to focus on a subset of the request parts. + +If you do not want to document a request part, you can mark it as ignored. +This prevents it from appearing in the generated snippet while avoiding the failure described earlier. + + + +[[documenting-your-api-request-parts-payloads]] +=== Request Part Payloads + +You can document the payload of a request part in much the same way as the <>, with support for documenting a request part's body and its fields. + + + +[[documenting-your-api-request-parts-payloads-body]] +==== Documenting a Request Part's Body + +You can generate a snippet containing the body of a request part as follows: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=body] +---- +<1> Configure Spring REST docs to produce a snippet containing the body of the request part named `metadata`. +Uses the static `requestPartBody` method on `PayloadDocumentation`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/RequestPartPayload.java[tags=body] +---- +<1> Configure Spring REST docs to produce a snippet containing the body of the request part named `metadata`. +Uses the static `requestPartBody` method on `PayloadDocumentation`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=body] +---- +<1> Configure Spring REST docs to produce a snippet containing the body of the request part named `metadata`. +Uses the static `requestPartBody` method on `PayloadDocumentation`. + +The result is a snippet named `request-part-${part-name}-body.adoc` that contains the part's body. +For example, documenting a part named `metadata` produces a snippet named `request-part-metadata-body.adoc`. + + + +[[documenting-your-api-request-parts-payloads-fields]] +==== Documenting a Request Part's Fields + +You can document a request part's fields in much the same way as the fields of a request or response, as follows: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=fields] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the payload of the request part named `metadata`. +Uses the static `requestPartFields` method on `PayloadDocumentation`. +<2> Expect a field with the path `version`. +Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/RequestPartPayload.java[tags=fields] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the payload of the request part named `metadata`. +Uses the static `requestPartFields` method on `PayloadDocumentation`. +<2> Expect a field with the path `version`. +Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=fields] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the payload of the request part named `metadata`. +Uses the static `requestPartFields` method on `PayloadDocumentation`. +<2> Expect a field with the path `version`. +Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. -When documenting request parts, the test will fail if an undocumented part is used in the -request. Similarly, the test will also fail if a documented part is not found in the -request and the part has not been marked as optional. +The result is a snippet that contains a table describing the part's fields. +This snippet is named `request-part-${part-name}-fields.adoc`. +For example, documenting a part named `metadata` produces a snippet named `request-part-metadata-fields.adoc`. -Request parts can also be documented in a relaxed mode where any undocumented -parts will not cause a test failure. To do so, use the `relaxedRequestParts` method on -`org.springframework.restdocs.request.RequestDocumentation`. This can be useful -when documenting a particular scenario where you only want to focus on a subset of the -request parts. +When documenting fields, the test fails if an undocumented field is found in the payload of the part. +Similarly, the test also fails if a documented field is not found in the payload of the part and the field has not been marked as optional. +For payloads with a hierarchical structure, documenting a field is sufficient for all of its descendants to also be treated as having been documented. -If you do not want to document a request part, you can mark it as ignored. This will -prevent it from appearing in the generated snippet while avoiding the failure described -above. +If you do not want to document a field, you can mark it as ignored. +Doing so prevents it from appearing in the generated snippet while avoiding the failure described above. + +You can also document fields in a relaxed mode, where any undocumented fields do not cause a test failure. +To do so, use the `relaxedRequestPartFields` method on `org.springframework.restdocs.payload.PayloadDocumentation`. +This can be useful when documenting a particular scenario where you only want to focus on a subset of the payload of the part. + +For further information on describing fields, documenting payloads that use XML, and more, see the <>. [[documenting-your-api-http-headers]] -=== HTTP headers +=== HTTP Headers -The headers in a request or response can be documented using `requestHeaders` and -`responseHeaders` respectively. For example: +You can document the headers in a request or response by using `requestHeaders` and `responseHeaders`, respectively. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/HttpHeaders.java[tags=headers] ---- -<1> Perform a `GET` request with an `Authorization` header that uses basic authentication +<1> Perform a `GET` request with an `Authorization` header that uses basic authentication. <2> Configure Spring REST Docs to produce a snippet describing the request's headers. - Uses the static `requestHeaders` method on - `org.springframework.restdocs.headers.HeaderDocumentation`. -<3> Document the `Authorization` header. Uses the static `headerWithName` method on - `org.springframework.restdocs.headers.HeaderDocumentation. -<4> Produce a snippet describing the response's headers. Uses the static `responseHeaders` - method on `org.springframework.restdocs.headers.HeaderDocumentation`. +Uses the static `requestHeaders` method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<3> Document the `Authorization` header. +Uses the static `headerWithName` method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<4> Produce a snippet describing the response's headers. +Uses the static `responseHeaders` method on `org.springframework.restdocs.headers.HeaderDocumentation`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/HttpHeaders.java[tags=headers] +---- +<1> Perform a `GET` request with an `Authorization` header that uses basic authentication. +<2> Configure Spring REST Docs to produce a snippet describing the request's headers. +Uses the static `requestHeaders` method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<3> Document the `Authorization` header. +Uses the static `headerWithName` method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<4> Produce a snippet describing the response's headers. +Uses the static `responseHeaders` method on `org.springframework.restdocs.headers.HeaderDocumentation`. [source,java,indent=0,role="secondary"] .REST Assured @@ -602,161 +1000,221 @@ include::{examples-dir}/com/example/mockmvc/HttpHeaders.java[tags=headers] include::{examples-dir}/com/example/restassured/HttpHeaders.java[tags=headers] ---- <1> Configure Spring REST Docs to produce a snippet describing the request's headers. - Uses the static `requestHeaders` method on - `org.springframework.restdocs.headers.HeaderDocumentation`. -<2> Document the `Authorization` header. Uses the static `headerWithName` method on - `org.springframework.restdocs.headers.HeaderDocumentation. -<3> Produce a snippet describing the response's headers. Uses the static `responseHeaders` - method on `org.springframework.restdocs.headers.HeaderDocumentation`. -<4> Configure the request with an `Authorization` header that uses basic authentication +Uses the static `requestHeaders` method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<2> Document the `Authorization` header. +Uses the static `headerWithName` method on `org.springframework.restdocs.headers.HeaderDocumentation. +<3> Produce a snippet describing the response's headers. +Uses the static `responseHeaders` method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<4> Configure the request with an `Authorization` header that uses basic authentication. + +The result is a snippet named `request-headers.adoc` and a snippet named `response-headers.adoc`. +Each contains a table describing the headers. -The result is a snippet named `request-headers.adoc` and a snippet named -`response-headers.adoc`. Each contains a table describing the headers. +When documenting HTTP Headers, the test fails if a documented header is not found in the request or response. -When documenting HTTP Headers, the test will fail if a documented header is not found in -the request or response. + + +[[documenting-your-api-http-cookies]] +=== HTTP Cookies + +You can document the cookies in a request or response by using `requestCookies` and `responseCookies`, respectively. +The following examples show how to do so: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/HttpCookies.java[tags=cookies] +---- +<1> Make a GET request with a `JSESSIONID` cookie. +<2> Configure Spring REST Docs to produce a snippet describing the request's cookies. + Uses the static `requestCookies` method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<3> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<4> Produce a snippet describing the response's cookies. + Uses the static `responseCookies` method on `org.springframework.restdocs.cookies.CookieDocumentation`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/HttpCookies.java[tags=cookies] +---- +<1> Make a GET request with a `JSESSIONID` cookie. +<2> Configure Spring REST Docs to produce a snippet describing the request's cookies. + Uses the static `requestCookies` method on + `org.springframework.restdocs.cookies.CookieDocumentation`. +<3> Document the `JSESSIONID` cookie. + Uses the static `cookieWithName` method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<4> Produce a snippet describing the response's cookies. + Uses the static `responseCookies` method on `org.springframework.restdocs.cookies.CookieDocumentation`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/HttpCookies.java[tags=cookies] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's cookies. + Uses the static `requestCookies` method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<2> Document the `JSESSIONID` cookie. + Uses the static `cookieWithName` method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<3> Produce a snippet describing the response's cookies. + Uses the static `responseCookies` method on `org.springframework.restdocs.cookies.CookieDocumentation`. +<4> Send a `JSESSIONID` cookie with the request. + +The result is a snippet named `request-cookies.adoc` and a snippet named `response-cookies.adoc`. +Each contains a table describing the cookies. + +When documenting HTTP cookies, the test fails if an undocumented cookie is found in the request or response. +Similarly, the test also fails if a documented cookie is not found and the cookie has not been marked as optional. +You can also document cookies in a relaxed mode, where any undocumented cookies do not cause a test failure. +To do so, use the `relaxedRequestCookies` and `relaxedResponseCookies` methods on `org.springframework.restdocs.cookies.CookieDocumentation`. +This can be useful when documenting a particular scenario where you only want to focus on a subset of the cookies. +If you do not want to document a cookie, you can mark it as ignored. +Doing so prevents it from appearing in the generated snippet while avoiding the failure described earlier. [[documenting-your-api-reusing-snippets]] -=== Reusing snippets +=== Reusing Snippets -It's common for an API that's being documented to have some features that are common -across several of its resources. To avoid repetition when documenting such resources a -`Snippet` configured with the common elements can be reused. +It is common for an API that is being documented to have some features that are common across several of its resources. +To avoid repetition when documenting such resources, you can reuse a `Snippet` configured with the common elements. -First, create the `Snippet` that describes the common elements. For example: +First, create the `Snippet` that describes the common elements. +The following example shows how to do so: [source,java,indent=0] ---- include::{examples-dir}/com/example/SnippetReuse.java[tags=field] ---- -Second, use this snippet and add further descriptors that are resource-specific. For -example: +Second, use this snippet and add further descriptors that are resource-specific. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/MockMvcSnippetReuse.java[tags=use] ---- -<1> Reuse the `pagingLinks` `Snippet` calling `and` to add descriptors that are specific - to the resource that is being documented. +<1> Reuse the `pagingLinks` `Snippet`, calling `and` to add descriptors that are specific to the resource that is being documented. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/WebTestClientSnippetReuse.java[tags=use] +---- +<1> Reuse the `pagingLinks` `Snippet`, calling `and` to add descriptors that are specific to the resource that is being documented. [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/RestAssuredSnippetReuse.java[tags=use] ---- -<1> Reuse the `pagingLinks` `Snippet` calling `and` to add descriptors that are specific - to the resource that is being documented. +<1> Reuse the `pagingLinks` `Snippet`, calling `and` to add descriptors that are specific to the resource that is being documented. -The result of the example is that links with the rels `first`, `last`, `next`, `previous`, -`alpha`, and `bravo` are all documented. +The result of the example is that links with `rel` values of `first`, `last`, `next`, `previous`, `alpha`, and `bravo` are all documented. [[documenting-your-api-constraints]] -=== Documenting constraints +=== Documenting Constraints Spring REST Docs provides a number of classes that can help you to document constraints. -An instance of `ConstraintDescriptions` can be used to access descriptions of a class's -constraints. For example: +You can use an instance of `ConstraintDescriptions` to access descriptions of a class's constraints. +The following example shows how to do so: [source,java,indent=0] ---- include::{examples-dir}/com/example/Constraints.java[tags=constraints] ---- -<1> Create an instance of `ConstraintDescriptions` for the `UserInput` class -<2> Get the descriptions of the name property's constraints. This list will contain two - descriptions; one for the `NotNull` constraint and one for the `Size` constraint. +<1> Create an instance of `ConstraintDescriptions` for the `UserInput` class. +<2> Get the descriptions of the `name` property's constraints. +This list contains two descriptions: one for the `NotNull` constraint and one for the `Size` constraint. -The {samples}/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`] -class in the Spring HATEOAS sample shows this functionality in action. +The {samples}/restful-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`] class in the Spring HATEOAS sample shows this functionality in action. [[documenting-your-api-constraints-finding]] -==== Finding constraints +==== Finding Constraints -By default, constraints are found using a Bean Validation `Validator`. Currently, only -property constraints are supported. You can customize the `Validator` that's used by -creating `ConstraintDescriptions` with a custom `ValidatorConstraintResolver` instance. -To take complete control of constraint resolution, your own implementation of -`ConstraintResolver` can be used. +By default, constraints are found by using a Bean Validation `Validator`. +Currently, only property constraints are supported. +You can customize the `Validator` that is used by creating `ConstraintDescriptions` with a custom `ValidatorConstraintResolver` instance. +To take complete control of constraint resolution, you can use your own implementation of `ConstraintResolver`. [[documenting-your-api-constraints-describing]] -==== Describing constraints - -Default descriptions are provided for all of the Bean Validation 1.1's constraints: - -* AssertFalse -* AssertTrue -* DecimalMax -* DecimalMin -* Digits -* Future -* Max -* Min -* NotNull -* Null -* Past -* Pattern -* Size - -Default descriptions are also provided for constraints from Hibernate Validator: - -* CreditCardNumber -* EAN -* Email -* Length -* LuhnCheck -* Mod10Check -* Mod11Check -* NotBlank -* NotEmpty -* Range -* SafeHtml -* URL - -To override the default descriptions, or to provide a new description, create a resource -bundle with the base name -`org.springframework.restdocs.constraints.ConstraintDescriptions`. The Spring -HATEOAS-based sample contains -{samples}/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties[an -example of such a resource bundle]. - -Each key in the resource bundle is the fully-qualified name of a constraint plus -`.description`. For example, the key for the standard `@NotNull` constraint is -`javax.validation.constraints.NotNull.description`. - -Property placeholder's referring to a constraint's attributes can be used in its -description. For example, the default description of the `@Min` constraint, -`Must be at least ${value}`, refers to the constraint's `value` attribute. - -To take more control of constraint description resolution, create `ConstraintDescriptions` -with a custom `ResourceBundleConstraintDescriptionResolver`. To take complete control, -create `ConstraintDescriptions` with a custom `ConstraintDescriptionResolver` -implementation. - - - -==== Using constraint descriptions in generated snippets - -Once you have a constraint's descriptions, you're free to use them however you like in -the generated snippets. For example, you may want to include the constraint descriptions -as part of a field's description. Alternatively, you could include the constraints as -<> in -the request fields snippet. The -{samples}/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`] -class in the Spring HATEOAS-based sample illustrates the latter approach. +==== Describing Constraints + +Default descriptions are provided for all of Bean Validation 3.1's constraints: + +* `AssertFalse` +* `AssertTrue` +* `DecimalMax` +* `DecimalMin` +* `Digits` +* `Email` +* `Future` +* `FutureOrPresent` +* `Max` +* `Min` +* `Negative` +* `NegativeOrZero` +* `NotBlank` +* `NotEmpty` +* `NotNull` +* `Null` +* `Past` +* `PastOrPresent` +* `Pattern` +* `Positive` +* `PositiveOrZero` +* `Size` + +Default descriptions are also provided for the following constraints from Hibernate +Validator: + +* `CodePointLength` +* `CreditCardNumber` +* `Currency` +* `EAN` +* `Email` +* `Length` +* `LuhnCheck` +* `Mod10Check` +* `Mod11Check` +* `NotBlank` +* `NotEmpty` +* `Currency` +* `Range` +* `SafeHtml` +* `URL` + +To override the default descriptions or to provide a new description, you can create a resource bundle with a base name of `org.springframework.restdocs.constraints.ConstraintDescriptions`. +The Spring HATEOAS-based sample contains {samples}/restful-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties[an example of such a resource bundle]. + +Each key in the resource bundle is the fully-qualified name of a constraint plus a `.description`. +For example, the key for the standard `@NotNull` constraint is `jakarta.validation.constraints.NotNull.description`. + +You can use a property placeholder referring to a constraint's attributes in its description. +For example, the default description of the `@Min` constraint, `Must be at least ${value}`, refers to the constraint's `value` attribute. + +To take more control of constraint description resolution, you can create `ConstraintDescriptions` with a custom `ResourceBundleConstraintDescriptionResolver`. +To take complete control, you can create `ConstraintDescriptions` with a custom `ConstraintDescriptionResolver` implementation. + + + +==== Using Constraint Descriptions in Generated Snippets + +Once you have a constraint's descriptions, you are free to use them however you like in the generated snippets. +For example, you may want to include the constraint descriptions as part of a field's description. +Alternatively, you could include the constraints as <> in the request fields snippet. +The {samples}/restful-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`] class in the Spring HATEOAS-based sample illustrates the latter approach. [[documenting-your-api-default-snippets]] -=== Default snippets +=== Default Snippets A number of snippets are produced automatically when you document a request and response. @@ -766,64 +1224,67 @@ A number of snippets are produced automatically when you document a request and | `curl-request.adoc` | Contains the https://curl.haxx.se[`curl`] command that is equivalent to the `MockMvc` -call that is being documented +call that is being documented. | `httpie-request.adoc` | Contains the https://httpie.org[`HTTPie`] command that is equivalent to the `MockMvc` -call that is being documented +call that is being documented. | `http-request.adoc` -| Contains the HTTP request that is equivalent to the `MockMvc` call that is being -documented +| Contains the HTTP request that is equivalent to the `MockMvc` call that is being documented. | `http-response.adoc` -| Contains the HTTP response that was returned +| Contains the HTTP response that was returned. + +| `request-body.adoc` +| Contains the body of the request that was sent. + +| `response-body.adoc` +| Contains the body of the response that was returned. |=== -You can configure which snippets are produced by default. Please refer to the -<> for more information. +You can configure which snippets are produced by default. +See the <> for more information. [[documentating-your-api-parameterized-output-directories]] -=== Using parameterized output directories +=== Using Parameterized Output Directories -The output directory used by `document` can be parameterized. The following parameters -are supported: +You can parameterize the output directory used by `document`. +The following parameters are supported: [cols="1,3"] |=== | Parameter | Description | {methodName} -| The unmodified name of the test method +| The unmodified name of the test method. | {method-name} -| The name of the test method, formatted using kebab-case +| The name of the test method, formatted using kebab-case. | {method_name} -| The name of the test method, formatted using snake_case +| The name of the test method, formatted using snake_case. | {ClassName} -| The unmodified simple name of the test class +| The unmodified simple name of the test class. | {class-name} -| The simple name of the test class, formatted using kebab-case +| The simple name of the test class, formatted using kebab-case. | {class_name} -| The simple name of the test class, formatted using snake_case +| The simple name of the test class, formatted using snake_case. | {step} -| The count of calls made to the service in the current test +| The count of calls made to the service in the current test. |=== -For example, `document("{class-name}/{method-name}")` in a test method named -`creatingANote` on the test class `GettingStartedDocumentation`, will write -snippets into a directory named `getting-started-documentation/creating-a-note`. +For example, `document("{class-name}/{method-name}")` in a test method named `creatingANote` on the test class `GettingStartedDocumentation` writes snippets into a directory named `getting-started-documentation/creating-a-note`. -A parameterized output directory is particularly useful in combination with an `@Before` -method. It allows documentation to be configured once in a setup method and then reused -in every test in the class: +A parameterized output directory is particularly useful in combination with a `@Before` method. +It lets documentation be configured once in a setup method and then reused in every test in the class. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc @@ -837,75 +1298,83 @@ include::{examples-dir}/com/example/mockmvc/ParameterizedOutput.java[tags=parame include::{examples-dir}/com/example/restassured/ParameterizedOutput.java[tags=parameterized-output] ---- -With this configuration in place, every call to the service you are testing will produce -the <> without any further -configuration. Take a look at the `GettingStartedDocumentation` classes in each of the -sample applications to see this functionality in action. +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/ParameterizedOutput.java[tags=parameterized-output] +---- + +With this configuration in place, every call to the service you are testing produces the <> without any further configuration. +Take a look at the `GettingStartedDocumentation` classes in each of the sample applications to see this functionality in action. [[documenting-your-api-customizing]] -=== Customizing the output +=== Customizing the Output + +This section describes how to customize the output of Spring REST Docs. [[documenting-your-api-customizing-snippets]] -==== Customizing the generated snippets -Spring REST Docs uses https://mustache.github.io[Mustache] templates to produce the -generated snippets. -{source}/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates[Default -templates] are provided for each of the snippets that Spring REST Docs can produce. To -customize a snippet's content, you can provide your own template. +==== Customizing the Generated Snippets + +Spring REST Docs uses https://mustache.github.io[Mustache] templates to produce the generated snippets. +{source}/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates[Default templates] are provided for each of the snippets that Spring REST Docs can produce. +To customize a snippet's content, you can provide your own template. -Templates are loaded from the classpath from an `org.springframework.restdocs.templates` -subpackage. The name of the subpackage is determined by the ID of the template format -that is in use. The default template format, Asciidoctor, has the ID `asciidoctor` so -snippets are loaded from `org.springframework.restdocs.templates.asciidoctor`. Each -template is named after the snippet that it will produce. For example, to -override the template for the `curl-request.adoc` snippet, create a template named -`curl-request.snippet` in -`src/test/resources/org/springframework/restdocs/templates/asciidoctor`. +Templates are loaded from the classpath from an `org.springframework.restdocs.templates` subpackage. +The name of the subpackage is determined by the ID of the template format that is in use. +The default template format, Asciidoctor, has an ID of `asciidoctor`, so snippets are loaded from `org.springframework.restdocs.templates.asciidoctor`. +Each template is named after the snippet that it produces. +For example, to override the template for the `curl-request.adoc` snippet, create a template named `curl-request.snippet` in `src/test/resources/org/springframework/restdocs/templates/asciidoctor`. [[documenting-your-api-customizing-including-extra-information]] -==== Including extra information +==== Including Extra Information There are two ways to provide extra information for inclusion in a generated snippet: -. Use the `attributes` method on a descriptor to add one or more attributes to it. -. Pass in some attributes when calling `curlRequest`, `httpRequest`, `httpResponse`, etc. - Such attributes will be associated with the snippet as a whole. +* Use the `attributes` method on a descriptor to add one or more attributes to it. +* Pass in some attributes when calling `curlRequest`, `httpRequest`, `httpResponse`, and so on. + Such attributes are associated with the snippet as a whole. Any additional attributes are made available during the template rendering process. -Coupled with a custom snippet template, this makes it possible to include extra -information in a generated snippet. +Coupled with a custom snippet template, this makes it possible to include extra information in a generated snippet. -A concrete example of the above is the addition of a constraints column and a title when -documenting request fields. The first step is to provide a `constraints` attribute for -each field that you are documenting and to provide a `title` attribute: +A concrete example is the addition of a constraints column and a title when documenting request fields. +The first step is to provide a `constraints` attribute for each field that you document and to provide a `title` attribute. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/Payload.java[tags=constraints] ---- -<1> Configure the `title` attribute for the request fields snippet -<2> Set the `constraints` attribute for the `name` field -<3> Set the `constraints` attribute for the `email` field +<1> Configure the `title` attribute for the request fields snippet. +<2> Set the `constraints` attribute for the `name` field. +<3> Set the `constraints` attribute for the `email` field. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/Payload.java[tags=constraints] +---- +<1> Configure the `title` attribute for the request fields snippet. +<2> Set the `constraints` attribute for the `name` field. +<3> Set the `constraints` attribute for the `email` field. [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/Payload.java[tags=constraints] ---- -<1> Configure the `title` attribute for the request fields snippet -<2> Set the `constraints` attribute for the `name` field -<3> Set the `constraints` attribute for the `email` field +<1> Configure the `title` attribute for the request fields snippet. +<2> Set the `constraints` attribute for the `name` field. +<3> Set the `constraints` attribute for the `email` field. -The second step is to provide a custom template named `request-fields.snippet` that -includes the information about the fields' constraints in the generated snippet's table -and adds a title: +The second step is to provide a custom template named `request-fields.snippet` that includes the information about the fields' constraints in the generated snippet's table and adds a title. [source,indent=0] ---- @@ -922,6 +1391,8 @@ and adds a title: {{/fields}} |=== ---- -<1> Add a title to the table -<2> Add a new column named "Constraints" -<3> Include the descriptors' `constraints` attribute in each row of the table \ No newline at end of file +<1> Add a title to the table. +<2> Add a new column named "Constraints". +<3> Include the descriptors' `constraints` attribute in each row of the table. + + diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index daa7739b5..11be6100d 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -6,83 +6,30 @@ This section describes how to get started with Spring REST Docs. [[getting-started-sample-applications]] -=== Sample applications +=== Sample Applications -If you want to jump straight in, a number of sample applications are available: +If you want to jump straight in, a number of https://github.com/spring-projects/spring-restdocs-samples[sample applications are available]. -[cols="3,2,10"] -.MockMvc -|=== -| Sample | Build system | Description - -| {samples}/rest-notes-spring-data-rest[Spring Data REST] -| Maven -| Demonstrates the creation of a getting started guide and an API guide for a service - implemented using https://projects.spring.io/spring-data-rest/[Spring Data REST]. - -| {samples}/rest-notes-spring-hateoas[Spring HATEOAS] -| Gradle -| Demonstrates the creation of a getting started guide and an API guide for a service - implemented using https://projects.spring.io/spring-hateoas/[Spring HATEOAS]. - -|=== - - -[cols="3,2,10"] -.REST Assured -|=== -| Sample | Build system | Description - -| {samples}/rest-notes-grails[Grails] -| Gradle -| Demonstrates the use of Spring REST docs with https://grails.org[Grails] and - https://github.com/spockframework/spock[Spock]. - -| {samples}/rest-assured[REST Assured] -| Gradle -| Demonstrates the use of Spring REST Docs with http://rest-assured.io[REST Assured]. - -|=== - - -[cols="3,2,10"] -.Advanced -|=== -| Sample | Build system | Description - -| {samples}/rest-notes-slate[Slate] -| Gradle -| Demonstrates the use of Spring REST Docs with Markdown and - https://github.com/tripit/slate[Slate]. - -| {samples}/testng[TestNG] -| Gradle -| Demonstrates the use of Spring REST Docs with http://testng.org[TestNG]. - -|=== [[getting-started-requirements]] === Requirements Spring REST Docs has the following minimum requirements: -- Java 7 -- Spring Framework 4.2 +* Java 17 +* Spring Framework 7 + +Additionally, the `spring-restdocs-restassured` module requires REST Assured 5.2. -Additionally, the `spring-restdocs-restassured` module has the following minimum -requirements: -- REST Assured 2.8 [[getting-started-build-configuration]] === Build configuration -The first step in using Spring REST Docs is to configure your project's build. The -{samples}/rest-notes-spring-hateoas[Spring HATEOAS] and -{samples}/rest-notes-spring-data-rest[Spring Data REST] samples contain a `build.gradle` -and `pom.xml` respectively that you may wish to use as a reference. The key parts of -the configuration are described below. +The first step in using Spring REST Docs is to configure your project's build. +The {samples}/restful-notes-spring-hateoas[Spring HATEOAS] and {samples}/restful-notes-spring-data-rest[Spring Data REST] samples contain a `build.gradle` and `pom.xml`, respectively, that you may wish to use as a reference. +The key parts of the configuration are described in the following listings: [source,xml,indent=0,subs="verbatim,attributes",role="primary"] .Maven @@ -94,96 +41,103 @@ the configuration are described below. test - <2> - ${project.build.directory}/generated-snippets - - - <3> + <2> org.asciidoctor asciidoctor-maven-plugin - 1.5.2 + 2.2.1 generate-docs - prepare-package <5> + prepare-package <3> process-asciidoc html book - - ${snippetsDirectory} <4> - + + <4> + org.springframework.restdocs + spring-restdocs-asciidoctor + {project-version} + + ---- -<1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. If you want to use - REST Assured rather than MockMvc, add a dependency on `spring-restdocs-restassured` - instead. -<2> Configure a property to define the output location for generated snippets. -<3> Add the Asciidoctor plugin -<4> Define an attribute named `snippets` that can be used when including the generated - snippets in your documentation. -<5> Using `prepare-package` allows the documentation to be - <>. +<1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. +If you want to use `WebTestClient` or REST Assured rather than MockMvc, add a dependency on `spring-restdocs-webtestclient` or `spring-restdocs-restassured` respectively instead. +<2> Add the Asciidoctor plugin. +<3> Using `prepare-package` allows the documentation to be <>. +<4> Add `spring-restdocs-asciidoctor` as a dependency of the Asciidoctor plugin. +This will automatically configure the `snippets` attribute for use in your `.adoc` files to point to `target/generated-snippets`. +It will also allow you to use the `operation` block macro. +It requires AsciidoctorJ 3.0. [source,indent=0,subs="verbatim,attributes",role="secondary"] .Gradle ---- plugins { <1> - id "org.asciidoctor.convert" version "1.5.2" + id "org.asciidoctor.jvm.convert" version "3.3.2" + } + + configurations { + asciidoctorExt <2> } - dependencies { <2> - testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' + dependencies { + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' <3> + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' <4> } - ext { <3> + ext { <5> snippetsDir = file('build/generated-snippets') } - test { <4> + test { <6> outputs.dir snippetsDir } - asciidoctor { <5> - attributes 'snippets': snippetsDir <6> - inputs.dir snippetsDir <7> - dependsOn test <8> + asciidoctor { <7> + inputs.dir snippetsDir <8> + configurations 'asciidoctorExt' <9> + dependsOn test <10> } ---- <1> Apply the Asciidoctor plugin. -<2> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. If - you want to use REST Assured rather than MockMvc, add a dependency on - `spring-restdocs-restassured` instead. -<3> Configure a property to define the output location for generated snippets. -<4> Configure the `test` task to add the snippets directory as an output. -<5> Configure the `asciidoctor` task -<6> Define an attribute named `snippets` that can be used when including the generated - snippets in your documentation. -<7> Configure the snippets directory as an input. -<8> Make the task depend on the test task so that the tests are run before the - documentation is created. +<2> Declare the `asciidoctorExt` configuration for dependencies that extend Asciidoctor. +<3> Add a dependency on `spring-restdocs-asciidoctor` in the `asciidoctorExt` configuration. +This will automatically configure the `snippets` attribute for use in your `.adoc` files to point to `build/generated-snippets`. +It will also allow you to use the `operation` block macro. +It requires AsciidoctorJ 3.0. +<4> Add a dependency on `spring-restdocs-mockmvc` in the `testImplementation` configuration. +If you want to use `WebTestClient` or REST Assured rather than MockMvc, add a dependency on `spring-restdocs-webtestclient` or `spring-restdocs-restassured` respectively instead. +<5> Configure a `snippetsDir` property that defines the output location for generated snippets. +<6> Make Gradle aware that running the `test` task will write output to the snippetsDir. This is required for https://docs.gradle.org/current/userguide/incremental_build.html[incremental builds]. +<7> Configure the `asciidoctor` task. +<8> Make Gradle aware that running the task will read input from the snippetsDir. This is required for https://docs.gradle.org/current/userguide/incremental_build.html[incremental builds]. +<9> Configure the use of the `asciidoctorExt` configuration for extensions. +<10> Make the task depend on the `test` task so that the tests are run before the documentation is created. + [[getting-started-build-configuration-packaging-the-documentation]] -==== Packaging the documentation +==== Packaging the Documentation -You may want to package the generated documentation in your project's jar file, for -example to have it {spring-boot-docs}/#boot-features-spring-mvc-static-content[served as -static content] by Spring Boot. To do so, configure your project's build so that: +You may want to package the generated documentation in your project's jar file -- for example, to have it {spring-boot-docs}/#boot-features-spring-mvc-static-content[served as static content] by Spring Boot. +To do so, configure your project's build so that: 1. The documentation is generated before the jar is built 2. The generated documentation is included in the jar +The following listings show how to do so in both Maven and Gradle: + [source,xml,indent=0,role="primary"] .Maven ---- @@ -194,7 +148,6 @@ static content] by Spring Boot. To do so, configure your project's build so that <2> maven-resources-plugin - 2.7 copy-resources @@ -219,18 +172,16 @@ static content] by Spring Boot. To do so, configure your project's build so that ---- <1> The existing declaration for the Asciidoctor plugin. -<2> The resource plugin must be declared after the Asciidoctor plugin as they are bound -to the same phase (`prepare-package`) and the resource plugin must run after the -Asciidoctor plugin to ensure that the documentation is generated before it's copied. -<3> Copy the generated documentation into the build output's `static/docs` directory, -from where it will be included in the jar file. +<2> The resource plugin must be declared after the Asciidoctor plugin as they are bound to the same phase (`prepare-package`) and the resource plugin must run after the Asciidoctor plugin to ensure that the documentation is generated before it's copied. +If you are not using Spring Boot and its plugin management, declare the plugin with an appropriate ``. +<3> Copy the generated documentation into the build output's `static/docs` directory, from where it will be included in the jar file. [source,indent=0,role="secondary"] .Gradle ---- - jar { + bootJar { dependsOn asciidoctor <1> - from ("${asciidoctor.outputDir}/html5") { <2> + from ("${asciidoctor.outputDir}") { <2> into 'static/docs' } } @@ -241,103 +192,118 @@ from where it will be included in the jar file. [[getting-started-documentation-snippets]] -=== Generating documentation snippets -Spring REST Docs uses -{spring-framework-docs}/#spring-mvc-test-framework[Spring's MVC Test framework] or -http://rest-assured.io/[REST Assured] to make requests to the service that you are -documenting. It then produces documentation snippets for the request and the resulting -response. +=== Generating Documentation Snippets + +Spring REST Docs uses Spring MVC's {spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework], Spring WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`], or https://rest-assured.io/[REST Assured] to make requests to the service that you are documenting. +It then produces documentation snippets for the request and the resulting response. [[getting-started-documentation-snippets-setup]] -==== Setting up your tests +==== Setting up Your Tests -Exactly how you setup your tests depends on the test framework that you're using. -Spring REST Docs provides first-class support for JUnit. Other frameworks, such as TestNG, -are also supported although slightly more setup is required. +Exactly how you set up your tests depends on the test framework that you use. +Spring REST Docs provides first-class support for JUnit 5. +Other frameworks, such as TestNG, are also supported, although slightly more setup is required. -[[getting-started-documentation-snippets-setup-junit]] -===== Setting up your JUnit tests -When using JUnit, the first step in generating documentation snippets is to declare a -`public` `JUnitRestDocumentation` field that's annotated as a JUnit `@Rule`. The -`JUnitRestDocumentation` rule is configured with the output directory into which generated -snippets should be written. This output directory should match the snippets directory that -you have configured in your `build.gradle` or `pom.xml` file. -For Maven (`pom.xml`) that will typically be `target/generated-snippets` and for -Gradle (`build.gradle`) it will typically be `build/generated-snippets`: +[[getting-started-documentation-snippets-setup-junit-5]] +===== Setting up Your JUnit 5 Tests -[source,java,indent=0,role="primary"] -.Maven +When using JUnit 5, the first step in generating documentation snippets is to apply the `RestDocumentationExtension` to your test class. +The following example shows how to do so: + +[source,java,indent=0] ---- -@Rule -public JUnitRestDocumentation restDocumentation = - new JUnitRestDocumentation("target/generated-snippets"); +@ExtendWith(RestDocumentationExtension.class) +public class JUnit5ExampleTests { ---- -[source,java,indent=0,role="secondary"] -.Gradle +When testing a typical Spring application, you should also apply the `SpringExtension`: + +[source,java,indent=0] +---- +@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) +public class JUnit5ExampleTests { ---- -@Rule -public JUnitRestDocumentation restDocumentation = - new JUnitRestDocumentation("build/generated-snippets"); + +The `RestDocumentationExtension` is automatically configured with an output directory based on your project's build tool: + +[cols="2,5"] +|=== +| Build tool | Output directory + +| Maven +| `target/generated-snippets` + +| Gradle +| `build/generated-snippets` +|=== + +If you are using JUnit 5.1, you can override the default by registering the extension as a field in your test class and providing an output directory when creating it. +The following example shows how to do so: + +[source,java,indent=0] ---- +public class JUnit5ExampleTests { -Next, provide an `@Before` method to configure MockMvc or REST Assured: + @RegisterExtension + final RestDocumentationExtension restDocumentation = new RestDocumentationExtension ("custom"); + +} +---- + +Next, you must provide a `@BeforeEach` method to configure MockMvc or WebTestClient, or REST Assured. +The following listings show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/ExampleApplicationTests.java[tags=setup] ---- -<1> The `MockMvc` instance is configured using a `MockMvcRestDocumentationConfigurer`. An -instance of this class can be obtained from the static `documentationConfiguration()` -method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. +<1> The `MockMvc` instance is configured by using a `MockMvcRestDocumentationConfigurer`. +You can obtain an instance of this class from the static `documentationConfiguration()` method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/ExampleApplicationTests.java[tags=setup] +---- +<1> The `WebTestClient` instance is configured by adding a `WebTestClientRestDocumentationConfigurer` as an `ExchangeFilterFunction`. +You can obtain an instance of this class from the static `documentationConfiguration()` method on `org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation`. [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/ExampleApplicationTests.java[tags=setup] ---- -<1> REST Assured is configured by adding a `RestAssuredRestDocumentationConfigurer` as a -`Filter`. An instance of this class can be obtained from the static -`documentationConfiguration()` method on -`org.springframework.restdocs.restassured.RestAssuredRestDocumentation`. +<1> REST Assured is configured by adding a `RestAssuredRestDocumentationConfigurer` as a `Filter`. +You can obtain an instance of this class from the static `documentationConfiguration()` method on `RestAssuredRestDocumentation` in the `org.springframework.restdocs.restassured` package. + +The configurer applies sensible defaults and also provides an API for customizing the configuration. +See the <> for more information. -The configurer applies sensible defaults and also provides an API for customizing the -configuration. Refer to the <> for more information. -===== Setting up your tests without JUnit [[getting-started-documentation-snippets-setup-manual]] +===== Setting up your tests without JUnit -The configuration when JUnit is not being used is largely similar to when it is being -used. This section describes the key differences. The {samples}/testng[TestNG sample] also -illustrates the approach. +The configuration when JUnit is not being used is a little more involved as the test class must perform some lifecycle management. +The {samples}/testng[TestNG sample] illustrates the approach. -The first difference is that `ManualRestDocumentation` should be used in place of -`JUnitRestDocumentation` and there's no need for the `@Rule` annotation: +First, you need a `ManualRestDocumentation` field. +The following example shows how to define it: -[source,java,indent=0,role="primary"] -.Maven ----- -private ManualRestDocumentation restDocumentation = - new ManualRestDocumentation("target/generated-snippets"); ----- - -[source,java,indent=0,role="secondary"] -.Gradle +[source,java,indent=0] ---- -private ManualRestDocumentation restDocumentation = - new ManualRestDocumentation("build/generated-snippets"); +private ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); ---- -Secondly, `ManualRestDocumentation.beforeTest(Class, String)` -must be called before each test. This can be done as part of the method that is -configuring MockMVC or REST Assured: +Secondly, you must call `ManualRestDocumentation.beforeTest(Class, String)` before each test. +You can do so as part of the method that configures MockMvc, WebTestClient, or REST Assured. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc @@ -345,74 +311,89 @@ configuring MockMVC or REST Assured: include::{examples-dir}/com/example/mockmvc/ExampleApplicationTestNgTests.java[tags=setup] ---- +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/ExampleApplicationTestNgTests.java[tags=setup] +---- + [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/ExampleApplicationTestNgTests.java[tags=setup] ---- -Lastly, `ManualRestDocumentation.afterTest` must be called after each test. For example, -with TestNG: +Finally, you must call `ManualRestDocumentation.afterTest` after each test. +The following example shows how to do so with TestNG: [source,java,indent=0] ---- -include::{examples-dir}/com/example/restassured/ExampleApplicationTestNgTests.java[tags=teardown] +include::{examples-dir}/com/example/mockmvc/ExampleApplicationTestNgTests.java[tags=teardown] ---- + + [[getting-started-documentation-snippets-invoking-the-service]] -==== Invoking the RESTful service +==== Invoking the RESTful Service -Now that the testing framework has been configured, it can be used to invoke the RESTful -service and document the request and response. For example: +Now that you have configured the testing framework, you can use it to invoke the RESTful service and document the request and response. +The following examples show how to do so: [source,java,indent=0,role="primary"] .MockMvc ---- include::{examples-dir}/com/example/mockmvc/InvokeService.java[tags=invoke-service] ---- -<1> Invoke the root (`/`) of the service and indicate that an `application/json` response -is required. +<1> Invoke the root (`/`) of the service and indicate that an `application/json` response is required. +<2> Assert that the service produced the expected response. +<3> Document the call to the service, writing the snippets into a directory named `index` (which is located beneath the configured output directory). +The snippets are written by a `RestDocumentationResultHandler`. +You can obtain an instance of this class from the static `document` method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. + +[source,java,indent=0,role="secondary"] +.WebTestClient +---- +include::{examples-dir}/com/example/webtestclient/InvokeService.java[tags=invoke-service] +---- +<1> Invoke the root (`/`) of the service and indicate that an `application/json` response is required. <2> Assert that the service produced the expected response. -<3> Document the call to the service, writing the snippets into a directory named `index` -that will be located beneath the configured output directory. The snippets are written by -a `RestDocumentationResultHandler`. An instance of this class can be obtained from the -static `document` method on -`org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. +<3> Document the call to the service, writing the snippets into a directory named `index` (which is located beneath the configured output directory). +The snippets are written by a `Consumer` of the `ExchangeResult`. +You can obtain such a consumer from the static `document` method on `org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation`. [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/InvokeService.java[tags=invoke-service] ---- -<1> Apply the specification that was initialised in the `@Before` method. +<1> Apply the specification that was initialized in the `@Before` method. <2> Indicate that an `application/json` response is required. -<3> Document the call to the service, writing the snippets into a directory named `index` -that will be located beneath the configured output directory. The snippets are written by -a `RestDocumentationFilter`. An instance of this class can be obtained from the -static `document` method on -`org.springframework.restdocs.restassured.RestAssuredRestDocumentation`. +<3> Document the call to the service, writing the snippets into a directory named `index` (which is located beneath the configured output directory). +The snippets are written by a `RestDocumentationFilter`. +You can obtain an instance of this class from the static `document` method on `RestAssuredRestDocumentation` in the `org.springframework.restdocs.restassured` package. <4> Invoke the root (`/`) of the service. <5> Assert that the service produce the expected response. -By default, four snippets are written: +By default, six snippets are written: * `/index/curl-request.adoc` * `/index/http-request.adoc` * `/index/http-response.adoc` * `/index/httpie-request.adoc` + * `/index/request-body.adoc` + * `/index/response-body.adoc` -Refer to <> for more information about these and other snippets -that can be produced by Spring REST Docs. +See <> for more information about these and other snippets that can be produced by Spring REST Docs. [[getting-started-using-the-snippets]] -=== Using the snippets +=== Using the Snippets -Before using the generated snippets, a `.adoc` source file must be created. You can name -the file whatever you like as long as it has a `.adoc` suffix. The result HTML file will -have the same name but with a `.html` suffix. The default location of the source files and -the resulting HTML files depends on whether you are using Maven or Gradle: +Before using the generated snippets, you must create an `.adoc` source file. +You can name the file whatever you like as long as it has a `.adoc` suffix. +The resulting HTML file has the same name but with a `.html` suffix. +The default location of the source files and the resulting HTML files depends on whether you use Maven or Gradle: [cols="2,5,8"] |=== @@ -425,16 +406,15 @@ the resulting HTML files depends on whether you are using Maven or Gradle: | Gradle | `src/docs/asciidoc/*.adoc` | `build/asciidoc/html5/*.html` - |=== -The generated snippets can then be included in the manually created Asciidoctor file from -above using the -https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro]. -The `snippets` attribute specified in the <> can be used to reference the snippets output directory. For example: +You can then include the generated snippets in the manually created Asciidoc file (described earlier in this section) by using the https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro]. +You can use the `snippets` attribute that is automatically set by `spring-restdocs-asciidoctor` configured in the <> to reference the snippets output directory. +The following example shows how to do so: [source,adoc,indent=0] ---- \include::{snippets}/index/curl-request.adoc[] ---- + + diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 128c448d1..c231d8728 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -1,5 +1,5 @@ = Spring REST Docs -Andy Wilkinson +Andy Wilkinson; Jay Bryant :doctype: book :icons: font :source-highlighter: highlightjs @@ -10,15 +10,15 @@ Andy Wilkinson :examples-dir: ../../test/java :github: https://github.com/spring-projects/spring-restdocs :source: {github}/tree/{branch-or-tag} -:samples: {source}/samples +:samples: https://github.com/spring-projects/spring-restdocs-samples/tree/main :templates: {source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates :spring-boot-docs: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle -:spring-framework-docs: https://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle +:spring-framework-docs: https://docs.spring.io/spring-framework/docs/{spring-framework-version}/reference/html/ +:spring-framework-api: https://docs.spring.io/spring-framework/docs/{spring-framework-version}/javadoc-api [[abstract]] -Document RESTful services by combining hand-written documentation with auto-generated -snippets produced with Spring MVC Test. +Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test or WebTestClient. include::introduction.adoc[] include::getting-started.adoc[] @@ -27,4 +27,4 @@ include::customizing-requests-and-responses.adoc[] include::configuration.adoc[] include::working-with-asciidoctor.adoc[] include::working-with-markdown.adoc[] -include::contributing.adoc[] \ No newline at end of file +include::contributing.adoc[] diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc index a44c23398..eb40443be 100644 --- a/docs/src/docs/asciidoc/introduction.adoc +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -1,25 +1,22 @@ [[introduction]] == Introduction -The aim of Spring REST Docs is to help you to produce documentation for your RESTful -services that is accurate and readable. +The aim of Spring REST Docs is to help you produce accurate and readable documentation for your RESTful services. -Writing high-quality documentation is difficult. One way to ease that difficulty is to use -tools that are well-suited to the job. To this end, Spring REST Docs uses -https://asciidoctor.org[Asciidoctor] by default. Asciidoctor processes plain text and -produces HTML, styled and layed out to suit your needs. If you prefer, Spring REST Docs -can also be configured to use Markdown. +Writing high-quality documentation is difficult. +One way to ease that difficulty is to use tools that are well-suited to the job. +To this end, Spring REST Docs uses https://asciidoctor.org[Asciidoctor] by default. +Asciidoctor processes plain text and produces HTML, styled and laid out to suit your needs. +If you prefer, you can also configure Spring REST Docs to use Markdown. + +Spring REST Docs uses snippets produced by tests written with Spring MVC's {spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework], Spring WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`] or https://rest-assured.io/[REST Assured 5]. +This test-driven approach helps to guarantee the accuracy of your service's documentation. +If a snippet is incorrect, the test that produces it fails. + +Documenting a RESTful service is largely about describing its resources. +Two key parts of each resource's description are the details of the HTTP requests that it consumes and the HTTP responses that it produces. +Spring REST Docs lets you work with these resources and the HTTP requests and responses, shielding your documentation from the inner-details of your service's implementation. +This separation helps you document your service's API rather than its implementation. +It also frees you to evolve the implementation without having to rework the documentation. -Spring REST Docs makes use of snippets produced by tests written with -{spring-framework-docs}/#spring-mvc-test-framework[Spring MVC Test] or -http://rest-assured.io/[REST Assured]. This test-driven approach helps to guarantee the -accuracy of your service's documentation. If a snippet is incorrect the test that produces -it will fail. -Documenting a RESTful service is largely about describing its resources. Two key parts -of each resource's description are the details of the HTTP requests that it consumes -and the HTTP responses that it produces. Spring REST Docs allows you to work with these -resources and the HTTP requests and responses, shielding your documentation -from the inner-details of your service's implementation. This separation helps you to -document your service's API rather than its implementation. It also frees you to evolve -the implementation without having to rework the documentation. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index 54f6fd2c2..698d5e460 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -1,8 +1,10 @@ [[working-with-asciidoctor]] == Working with Asciidoctor -This section describes any aspects of working with Asciidoctor that are particularly -relevant to Spring REST Docs. +This section describes the aspects of working with Asciidoctor that are particularly relevant to Spring REST Docs. + +NOTE: Asciidoc is the document format. +Asciidoctor is the tool that produces content (usually as HTML) from Asciidoc files (which end with `.adoc`). @@ -15,12 +17,118 @@ relevant to Spring REST Docs. [[working-with-asciidoctor-including-snippets]] -=== Including snippets +=== Including Snippets + +This section covers how to include Asciidoc snippets. + + + +[[working-with-asciidoctor-including-snippets-operation]] +==== Including Multiple Snippets for an Operation + +You can use a macro named `operation` to import all or some of the snippets that have been generated for a specific operation. +It is made available by including `spring-restdocs-asciidoctor` in your project's <>. +`spring-restdocs-asciidoctor` requires AsciidoctorJ 3.0. + +The target of the macro is the name of the operation. +In its simplest form, you can use the macro to include all of the snippets for an operation, as shown in the following example: + +[source,indent=0] +---- +operation::index[] +---- + +You can use the operation macro also supports a `snippets` attribute. +The `snippets` attribute to select the snippets that should be included. +The attribute's value is a comma-separated list. +Each entry in the list should be the name of a snippet file (minus the `.adoc` suffix) to include. +For example, only the curl, HTTP request, and HTTP response snippets can be included, as shown in the following example: + +[source,indent=0] +---- +operation::index[snippets='curl-request,http-request,http-response'] +---- + +The preceding example is the equivalent of the following: + +[source,adoc,indent=0] +---- +[[example_curl_request]] +== Curl request + +\include::{snippets}/index/curl-request.adoc[] + +[[example_http_request]] +== HTTP request + +\include::{snippets}/index/http-request.adoc[] + +[[example_http_response]] +== HTTP response + +\include::{snippets}/index/http-response.adoc[] + +---- + + + +[[working-with-asciidoctor-including-snippets-operation-titles]] +===== Section Titles + +For each snippet that is included by using the `operation` macro, a section with a title is created. +Default titles are provided for the following built-in snippets: + +|=== +| Snippet | Title + +| `curl-request` +| Curl Request + +| `http-request` +| HTTP request + +| `http-response` +| HTTP response + +| `httpie-request` +| HTTPie request + +| `links` +| Links + +| `request-body` +| Request body + +| `request-fields` +| Request fields -The https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include -macro] is used to include generated snippets in your documentation. The `snippets` -attribute specified in the <> -can be used to reference the snippets output directory, for example: +| `response-body` +| Response body + +| `response-fields` +| Response fields +|=== + +For snippets not listed in the preceding table, a default title is generated by replacing `-` characters with spaces and capitalizing the first letter. +For example, the title for a snippet named `custom-snippet` `will be` "`Custom snippet`". + +You can customize the default titles by using document attributes. +The name of the attribute should be `operation-{snippet}-title`. +For example, to customize the title of the `curl-request` snippet to be "Example request", you can use the following attribute: + +[source,indent=0] +---- +:operation-curl-request-title: Example request +---- + + + +[[working-with-asciidoctor-including-snippets-individual]] +==== Including Individual Snippets + +The https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro] is used to include individual snippets in your documentation. +You can use the `snippets` attribute (which is automatically set by `spring-restdocs-asciidoctor` configured in the <>) to reference the snippets output directory. +The following example shows how to do so: [source,indent=0] ---- @@ -30,35 +138,33 @@ can be used to reference the snippets output directory, for example: [[working-with-asciidoctor-customizing-tables]] -=== Customizing tables +=== Customizing Tables -Many of the snippets contain a table in its default configuration. The appearance of the -table can be customized, either by providing some additional configuration when the -snippet is included or by using a custom snippet template. +Many of the snippets contain a table in its default configuration. +The appearance of the table can be customized, either by providing some additional configuration when the snippet is included or by using a custom snippet template. [[working-with-asciidoctor-customizing-tables-formatting-columns]] -==== Formatting columns +==== Formatting Columns -Asciidoctor has rich support for -https://asciidoctor.org/docs/user-manual/#cols-format[formatting a table's columns]. For -example, the widths of a table's columns can be specified using the `cols` attribute: +Asciidoctor has rich support for https://asciidoctor.org/docs/user-manual/#cols-format[formatting a table's columns]. +As the following example shows, you can specify the widths of a table's columns by using the `cols` attribute: [source,indent=0] ---- -[cols=1,3] <1> +[cols="1,3"] <1> \include::{snippets}/index/links.adoc[] ---- -<1> The table's width will be split across its two columns with the second column being -three times as wide as the first. +<1> The table's width is split across its two columns, with the second column being three times as wide as the first. [[working-with-asciidoctor-customizing-tables-title]] -==== Configuring the title +==== Configuring the Title -The title of a table can be specified using a line prefixed by a `.`: +You can specify the title of a table by using a line prefixed by a `.`. +The following example shows how to do so: [source,indent=0] ---- @@ -69,25 +175,25 @@ The title of a table can be specified using a line prefixed by a `.`: -[[working-with-asciidoctor-customizing-tables-title]] -==== Avoiding table formatting problems +[[working-with-asciidoctor-customizing-tables-formatting-problems]] +==== Avoiding Table Formatting Problems -Asciidoctor uses the `|` character to delimit cells in a table. This can cause problems -if you want a `|` to appear in a cell's contents. The problem can be avoided by -escaping the `|` with a backslash, i.e. by using `\|` rather than `|`. +Asciidoctor uses the `|` character to delimit cells in a table. +This can cause problems if you want a `|` to appear in a cell's contents. +You can avoid the problem by escaping the `|` with a backslash -- in other words, by using `\|` rather than `|`. -All of the default Asciidoctor snippet templates perform this escaping automatically -use a Mustache lamba named `tableCellContent`. If you write your own custom templates -you may want to use this lamba. For example, to escape `|` characters -in a cell that contains the value of a `description` attribute: +All of the default Asciidoctor snippet templates perform this escaping automatically by using a Mustache lamba named `tableCellContent`. +If you write your own custom templates you may want to use this lamba. +The following example shows how to escape `|` characters in a cell that contains the value of a `description` attribute: ---- | {{#tableCellContent}}{{description}}{{/tableCellContent}} ---- +[[working-with-asciidoctor-further-reading]] +=== Further Reading + +See the https://asciidoctor.org/docs/user-manual/#tables[Tables section of the Asciidoctor user manual] for more information about customizing tables. -==== Further reading -Refer to the https://asciidoctor.org/docs/user-manual/#tables[Tables section of -the Asciidoctor user manual] for more information about customizing tables. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/working-with-markdown.adoc b/docs/src/docs/asciidoc/working-with-markdown.adoc index 6f6d8ed78..ed09c0574 100644 --- a/docs/src/docs/asciidoc/working-with-markdown.adoc +++ b/docs/src/docs/asciidoc/working-with-markdown.adoc @@ -1,28 +1,26 @@ [[working-with-markdown]] == Working with Markdown -This section describes any aspects of working with Markdown that are particularly -relevant to Spring REST Docs. +This section describes the aspects of working with Markdown that are particularly relevant to Spring REST Docs. [[working-with-markdown-limitations]] === Limitations -Markdown was originally designed for people writing for the web and, as such, isn't -as well-suited to writing documentation as Asciidoctor. Typically, these limitations -are overcome by using another tool that builds on top of Markdown. +Markdown was originally designed for people writing for the web and, as such, is not as well-suited to writing documentation as Asciidoctor. +Typically, these limitations are overcome by using another tool that builds on top of Markdown. -Markdown has no official support for tables. Spring REST Docs' default Markdown snippet -templates use https://michelf.ca/projects/php-markdown/extra/#table[Markdown Extra's table -format]. +Markdown has no official support for tables. +Spring REST Docs' default Markdown snippet templates use https://michelf.ca/projects/php-markdown/extra/#table[Markdown Extra's table format]. [[working-with-markdown-including-snippets]] -=== Including snippets +=== Including Snippets + +Markdown has no built-in support for including one Markdown file in another. +To include the generated snippets of Markdown in your documentation, you should use an additional tool that supports this functionality. +One example that is particularly well-suited to documenting APIs is https://github.com/tripit/slate[Slate]. + -Markdown has no built-in support for including one Markdown file in another. To include -the generated snippets of Markdown in your documentation, you should use an additional -tool that supports this functionality. One example that's particularly well-suited to -documenting APIs is https://github.com/tripit/slate[Slate]. \ No newline at end of file diff --git a/docs/src/test/java/com/example/Constraints.java b/docs/src/test/java/com/example/Constraints.java index 649128745..6d8ef1fcd 100644 --- a/docs/src/test/java/com/example/Constraints.java +++ b/docs/src/test/java/com/example/Constraints.java @@ -18,8 +18,8 @@ import java.util.List; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import org.springframework.restdocs.constraints.ConstraintDescriptions; diff --git a/docs/src/test/java/com/example/Hypermedia.java b/docs/src/test/java/com/example/Hypermedia.java index 71fe80588..041c44aff 100644 --- a/docs/src/test/java/com/example/Hypermedia.java +++ b/docs/src/test/java/com/example/Hypermedia.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,12 +22,16 @@ import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -public class Hypermedia { +public final class Hypermedia { + + private Hypermedia() { + + } // tag::ignore-links[] public static LinksSnippet links(LinkDescriptor... descriptors) { - return HypermediaDocumentation.links(linkWithRel("_self").ignored().optional(), - linkWithRel("curies").ignored()).and(descriptors); + return HypermediaDocumentation.links(linkWithRel("self").ignored().optional(), linkWithRel("curies").ignored()) + .and(descriptors); } // end::ignore-links[] diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index 69c7fc0d2..321761a35 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,17 +18,24 @@ import org.springframework.restdocs.payload.FieldDescriptor; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; public class Payload { @SuppressWarnings("unused") public void bookFieldDescriptors() { // tag::book-descriptors[] - FieldDescriptor[] book = new FieldDescriptor[] { - fieldWithPath("title").description("Title of the book"), + FieldDescriptor[] book = new FieldDescriptor[] { fieldWithPath("title").description("Title of the book"), fieldWithPath("author").description("Author of the book") }; // end::book-descriptors[] } + public void customSubsectionId() { + // tag::custom-subsection-id[] + responseBody(beneathPath("weather.temperature").withSubsectionId("temp")); + // end::custom-subsection-id[] + } + } diff --git a/docs/src/test/java/com/example/SnippetReuse.java b/docs/src/test/java/com/example/SnippetReuse.java index 606851540..ca87eca03 100644 --- a/docs/src/test/java/com/example/SnippetReuse.java +++ b/docs/src/test/java/com/example/SnippetReuse.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ public class SnippetReuse { linkWithRel("last").optional().description("The last page of results"), linkWithRel("next").optional().description("The next page of results"), linkWithRel("prev").optional().description("The previous page of results")); + // end::field[] } diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultOperationPreprocessors.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultOperationPreprocessors.java new file mode 100644 index 000000000..5aec4248d --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultOperationPreprocessors.java @@ -0,0 +1,51 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package com.example.mockmvc; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@ExtendWith(RestDocumentationExtension.class) +class CustomDefaultOperationPreprocessors { + + private WebApplicationContext context; + + @SuppressWarnings("unused") + private MockMvc mockMvc; + + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { + // tag::custom-default-operation-preprocessors[] + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(restDocumentation).operationPreprocessors() + .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> + .withResponseDefaults(prettyPrint())) // <2> + .build(); + // end::custom-default-operation-preprocessors[] + } + +} diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index 230d77bd0..860e8e1e2 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,12 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -28,11 +29,8 @@ import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -public class CustomDefaultSnippets { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build"); +@ExtendWith(RestDocumentationExtension.class) +class CustomDefaultSnippets { @Autowired private WebApplicationContext context; @@ -40,13 +38,12 @@ public class CustomDefaultSnippets { @SuppressWarnings("unused") private MockMvc mockMvc; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-default-snippets[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).snippets() - .withDefaults(curlRequest())) - .build(); + .apply(documentationConfiguration(restDocumentation).snippets().withDefaults(curlRequest())) + .build(); // end::custom-default-snippets[] } diff --git a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java index 0780173fe..7b6d1a42d 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java +++ b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,20 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -public class CustomEncoding { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); +@ExtendWith(RestDocumentationExtension.class) +class CustomEncoding { @Autowired private WebApplicationContext context; @@ -37,13 +37,12 @@ public class CustomEncoding { @SuppressWarnings("unused") private MockMvc mockMvc; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-encoding[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation) - .snippets().withEncoding("ISO-8859-1")) - .build(); + .apply(documentationConfiguration(restDocumentation).snippets().withEncoding("ISO-8859-1")) + .build(); // end::custom-encoding[] } diff --git a/docs/src/test/java/com/example/mockmvc/CustomFormat.java b/docs/src/test/java/com/example/mockmvc/CustomFormat.java index 552f1ac53..75a304654 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomFormat.java +++ b/docs/src/test/java/com/example/mockmvc/CustomFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,12 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -27,10 +29,8 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -public class CustomFormat { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); +@ExtendWith(RestDocumentationExtension.class) +class CustomFormat { @Autowired private WebApplicationContext context; @@ -38,13 +38,13 @@ public class CustomFormat { @SuppressWarnings("unused") private MockMvc mockMvc; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-format[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation) - .snippets().withTemplateFormat(TemplateFormats.markdown())) - .build(); + .apply(documentationConfiguration(restDocumentation).snippets() + .withTemplateFormat(TemplateFormats.markdown())) + .build(); // end::custom-format[] } diff --git a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java index 31e53ab64..7af440b08 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,20 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -public class CustomUriConfiguration { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); +@ExtendWith(RestDocumentationExtension.class) +class CustomUriConfiguration { @Autowired private WebApplicationContext context; @@ -37,15 +37,15 @@ public class CustomUriConfiguration { @SuppressWarnings("unused") private MockMvc mockMvc; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-uri-configuration[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).uris() - .withScheme("https") - .withHost("example.com") - .withPort(443)) - .build(); + .apply(documentationConfiguration(restDocumentation).uris() + .withScheme("https") + .withHost("example.com") + .withPort(443)) + .build(); // end::custom-uri-configuration[] } diff --git a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index 307d0ea76..dcf4d44af 100644 --- a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,11 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -29,45 +30,33 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@ExtendWith(RestDocumentationExtension.class) public class EveryTestPreprocessing { - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "target/generated-snippets"); - private WebApplicationContext context; // tag::setup[] private MockMvc mockMvc; - private RestDocumentationResultHandler documentationHandler; - - @Before - public void setup() { - this.documentationHandler = document("{method-name}", // <1> - preprocessRequest(removeHeaders("Foo")), - preprocessResponse(prettyPrint())); + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(this.documentationHandler) // <2> - .build(); + .apply(documentationConfiguration(restDocumentation).operationPreprocessors() + .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> + .withResponseDefaults(prettyPrint())) // <2> + .build(); } - // end::setup[] public void use() throws Exception { // tag::use[] - this.mockMvc.perform(get("/")) // <1> - .andExpect(status().isOk()) - .andDo(this.documentationHandler.document( // <2> - links(linkWithRel("self").description("Canonical self link")) - )); + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andDo(document("index", links(linkWithRel("self").description("Canonical self link")))); // end::use[] } diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java index 96bf11310..4b3eb6173 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,32 +18,33 @@ import java.lang.reflect.Method; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; public class ExampleApplicationTestNgTests { - public final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( - "target/generated-snippets"); + public final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); + @SuppressWarnings("unused") // tag::setup[] private MockMvc mockMvc; - + @Autowired private WebApplicationContext context; @BeforeMethod public void setUp(Method method) { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) - .build(); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); this.restDocumentation.beforeTest(getClass(), method.getName()); } diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java index c7307657f..24c0a3f4b 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,35 +16,29 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -public class ExampleApplicationTests { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "target/generated-snippets"); +@ExtendWith(RestDocumentationExtension.class) +class ExampleApplicationTests { @SuppressWarnings("unused") // tag::setup[] private MockMvc mockMvc; - @Autowired - private WebApplicationContext context; - - @Before - public void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) // <1> - .build(); + @BeforeEach + void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .apply(documentationConfiguration(restDocumentation)) // <1> + .build(); } // end::setup[] diff --git a/docs/src/test/java/com/example/mockmvc/FormParameters.java b/docs/src/test/java/com/example/mockmvc/FormParameters.java new file mode 100644 index 000000000..ff1472c1c --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/FormParameters.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package com.example.mockmvc; + +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.request.RequestDocumentation.formParameters; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class FormParameters { + + private MockMvc mockMvc; + + public void postFormDataSnippet() throws Exception { + // tag::form-parameters[] + this.mockMvc.perform(post("/users").param("username", "Tester")) // <1> + .andExpect(status().isCreated()) + .andDo(document("create-user", formParameters(// <2> + parameterWithName("username").description("The user's username") // <3> + ))); + // end::form-parameters[] + } + +} diff --git a/docs/src/test/java/com/example/mockmvc/HttpCookies.java b/docs/src/test/java/com/example/mockmvc/HttpCookies.java new file mode 100644 index 000000000..ad609bf59 --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/HttpCookies.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package com.example.mockmvc; + +import jakarta.servlet.http.Cookie; + +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class HttpCookies { + + private MockMvc mockMvc; + + public void cookies() throws Exception { + // tag::cookies[] + this.mockMvc.perform(get("/").cookie(new Cookie("JSESSIONID", "ACBCDFD0FF93D5BB"))) // <1> + .andExpect(status().isOk()) + .andDo(document("cookies", requestCookies(// <2> + cookieWithName("JSESSIONID").description("Session token")), // <3> + responseCookies(// <4> + cookieWithName("JSESSIONID").description("Updated session token"), + cookieWithName("logged_in") + .description("Set to true if the user is currently logged in")))); + // end::cookies[] + } + +} diff --git a/docs/src/test/java/com/example/mockmvc/HttpHeaders.java b/docs/src/test/java/com/example/mockmvc/HttpHeaders.java index f0198430a..1395d6366 100644 --- a/docs/src/test/java/com/example/mockmvc/HttpHeaders.java +++ b/docs/src/test/java/com/example/mockmvc/HttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,20 +31,18 @@ public class HttpHeaders { public void headers() throws Exception { // tag::headers[] - this.mockMvc - .perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) // <1> + this.mockMvc.perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) // <1> .andExpect(status().isOk()) - .andDo(document("headers", - requestHeaders( // <2> - headerWithName("Authorization").description( - "Basic auth credentials")), // <3> - responseHeaders( // <4> - headerWithName("X-RateLimit-Limit").description( - "The total number of requests permitted per period"), - headerWithName("X-RateLimit-Remaining").description( - "Remaining requests permitted in current period"), - headerWithName("X-RateLimit-Reset").description( - "Time at which the rate limit period will reset")))); + .andDo(document("headers", requestHeaders(// <2> + headerWithName("Authorization").description("Basic auth credentials")), // <3> + responseHeaders(// <4> + headerWithName("X-RateLimit-Limit") + .description("The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining") + .description("Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset") + .description("Time at which the rate limit period will reset")))); // end::headers[] } + } diff --git a/docs/src/test/java/com/example/mockmvc/Hypermedia.java b/docs/src/test/java/com/example/mockmvc/Hypermedia.java index 1035c4ea2..5993f900e 100644 --- a/docs/src/test/java/com/example/mockmvc/Hypermedia.java +++ b/docs/src/test/java/com/example/mockmvc/Hypermedia.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,15 @@ package com.example.mockmvc; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; - -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class Hypermedia { @@ -34,7 +34,7 @@ public void defaultExtractor() throws Exception { // tag::links[] this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("index", links( // <1> + .andDo(document("index", links(// <1> linkWithRel("alpha").description("Link to the alpha resource"), // <2> linkWithRel("bravo").description("Link to the bravo resource")))); // <3> // end::links[] @@ -43,12 +43,11 @@ public void defaultExtractor() throws Exception { public void explicitExtractor() throws Exception { this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - //tag::explicit-extractor[] + // tag::explicit-extractor[] .andDo(document("index", links(halLinks(), // <1> linkWithRel("alpha").description("Link to the alpha resource"), linkWithRel("bravo").description("Link to the bravo resource")))); - // end::explicit-extractor[] + // end::explicit-extractor[] } - } diff --git a/docs/src/test/java/com/example/mockmvc/InvokeService.java b/docs/src/test/java/com/example/mockmvc/InvokeService.java index dbf3dc30f..bd8d265af 100644 --- a/docs/src/test/java/com/example/mockmvc/InvokeService.java +++ b/docs/src/test/java/com/example/mockmvc/InvokeService.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package com.example.mockmvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - public class InvokeService { private MockMvc mockMvc; diff --git a/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java b/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java index 6e8630854..a81f28275 100644 --- a/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java +++ b/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ public void documentation() throws Exception { // tag::use[] this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("example", this.pagingLinks.and( // <1> + .andDo(document("example", this.pagingLinks.and(// <1> linkWithRel("alpha").description("Link to the alpha resource"), linkWithRel("bravo").description("Link to the bravo resource")))); // end::use[] diff --git a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java index 53a94b0c6..831c9a4ae 100644 --- a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,11 @@ package com.example.mockmvc; -import org.junit.Before; -import org.junit.Rule; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -26,10 +28,8 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -public class ParameterizedOutput { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); +@ExtendWith(RestDocumentationExtension.class) +class ParameterizedOutput { @SuppressWarnings("unused") private MockMvc mockMvc; @@ -37,11 +37,12 @@ public class ParameterizedOutput { private WebApplicationContext context; // tag::parameterized-output[] - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(document("{method-name}/{step}/")).build(); + .apply(documentationConfiguration(restDocumentation)) + .alwaysDo(document("{method-name}/{step}/")) + .build(); } // end::parameterized-output[] diff --git a/docs/src/test/java/com/example/mockmvc/PathParameters.java b/docs/src/test/java/com/example/mockmvc/PathParameters.java index 446529909..22135e60b 100644 --- a/docs/src/test/java/com/example/mockmvc/PathParameters.java +++ b/docs/src/test/java/com/example/mockmvc/PathParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,14 @@ package com.example.mockmvc; +import org.springframework.test.web.servlet.MockMvc; + import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.springframework.test.web.servlet.MockMvc; - public class PathParameters { private MockMvc mockMvc; @@ -32,7 +32,7 @@ public void pathParametersSnippet() throws Exception { // tag::path-parameters[] this.mockMvc.perform(get("/locations/{latitude}/{longitude}", 51.5072, 0.1275)) // <1> .andExpect(status().isOk()) - .andDo(document("locations", pathParameters( // <2> + .andDo(document("locations", pathParameters(// <2> parameterWithName("latitude").description("The location's latitude"), // <3> parameterWithName("longitude").description("The location's longitude") // <4> ))); diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index 1f931b7d8..3ce122d7d 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,24 @@ package com.example.mockmvc; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; + import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.springframework.http.MediaType; -import org.springframework.restdocs.payload.FieldDescriptor; -import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.web.servlet.MockMvc; - public class Payload { private MockMvc mockMvc; @@ -39,56 +42,77 @@ public void response() throws Exception { // tag::response[] this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("index", responseFields( // <1> - fieldWithPath("contact").description("The user's contact details"), // <2> - fieldWithPath("contact.email").description("The user's email address")))); // <3> + .andDo(document("index", responseFields(// <1> + fieldWithPath("contact.email").description("The user's email address"), // <2> + fieldWithPath("contact.name").description("The user's name")))); // <3> // end::response[] } + public void subsection() throws Exception { + // tag::subsection[] + this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("index", responseFields(// <1> + subsectionWithPath("contact").description("The user's contact details")))); // <1> + // end::subsection[] + } + public void explicitType() throws Exception { this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // tag::explicit-type[] - .andDo(document("index", responseFields( - fieldWithPath("contact.email") - .type(JsonFieldType.STRING) // <1> - .description("The user's email address")))); - // end::explicit-type[] + .andDo(document("index", responseFields(fieldWithPath("contact.email").type(JsonFieldType.STRING) // <1> + .description("The user's email address")))); + // end::explicit-type[] } public void constraints() throws Exception { this.mockMvc.perform(post("/users/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // tag::constraints[] - .andDo(document("create-user", requestFields( - attributes(key("title").value("Fields for user creation")), // <1> + .andDo(document("create-user", requestFields(attributes(key("title").value("Fields for user creation")), // <1> fieldWithPath("name").description("The user's name") - .attributes(key("constraints") - .value("Must not be null. Must not be empty")), // <2> + .attributes(key("constraints").value("Must not be null. Must not be empty")), // <2> fieldWithPath("email").description("The user's email address") - .attributes(key("constraints") - .value("Must be a valid email address"))))); // <3> - // end::constraints[] + .attributes(key("constraints").value("Must be a valid email address"))))); // <3> + // end::constraints[] } public void descriptorReuse() throws Exception { - FieldDescriptor[] book = new FieldDescriptor[] { - fieldWithPath("title").description("Title of the book"), + FieldDescriptor[] book = new FieldDescriptor[] { fieldWithPath("title").description("Title of the book"), fieldWithPath("author").description("Author of the book") }; // tag::single-book[] this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("book", responseFields(book))); // <1> + .andExpect(status().isOk()) + .andDo(document("book", responseFields(book))); // <1> // end::single-book[] // tag::book-array[] this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("book", responseFields( - fieldWithPath("[]").description("An array of books")) // <1> - .andWithPrefix("[].", book))); // <2> + .andExpect(status().isOk()) + .andDo(document("book", responseFields(fieldWithPath("[]").description("An array of books")) // <1> + .andWithPrefix("[].", book))); // <2> // end::book-array[] } + public void fieldsSubsection() throws Exception { + // tag::fields-subsection[] + this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("location", responseFields(beneathPath("weather.temperature"), // <1> + fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> + fieldWithPath("low").description("The forecast low in degrees celcius")))); + // end::fields-subsection[] + } + + public void bodySubsection() throws Exception { + // tag::body-subsection[] + this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("location", responseBody(beneathPath("weather.temperature")))); // <1> + + // end::body-subsection[] + } + } diff --git a/docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java index 90eab9c9e..75f91610e 100644 --- a/docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,10 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class PerTestPreprocessing { @@ -32,8 +32,9 @@ public class PerTestPreprocessing { public void general() throws Exception { // tag::preprocessing[] - this.mockMvc.perform(get("/")).andExpect(status().isOk()) - .andDo(document("index", preprocessRequest(removeHeaders("Foo")), // <1> + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andDo(document("index", preprocessRequest(modifyHeaders().remove("Foo")), // <1> preprocessResponse(prettyPrint()))); // <2> // end::preprocessing[] } diff --git a/docs/src/test/java/com/example/mockmvc/RequestParameters.java b/docs/src/test/java/com/example/mockmvc/QueryParameters.java similarity index 66% rename from docs/src/test/java/com/example/mockmvc/RequestParameters.java rename to docs/src/test/java/com/example/mockmvc/QueryParameters.java index aea221735..68c60f917 100644 --- a/docs/src/test/java/com/example/mockmvc/RequestParameters.java +++ b/docs/src/test/java/com/example/mockmvc/QueryParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,38 +16,27 @@ package com.example.mockmvc; +import org.springframework.test.web.servlet.MockMvc; + import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.springframework.test.web.servlet.MockMvc; - -public class RequestParameters { +public class QueryParameters { private MockMvc mockMvc; public void getQueryStringSnippet() throws Exception { - // tag::request-parameters-query-string[] + // tag::query-parameters[] this.mockMvc.perform(get("/users?page=2&per_page=100")) // <1> .andExpect(status().isOk()) - .andDo(document("users", requestParameters( // <2> + .andDo(document("users", queryParameters(// <2> parameterWithName("page").description("The page to retrieve"), // <3> parameterWithName("per_page").description("Entries per page") // <4> ))); - // end::request-parameters-query-string[] - } - - public void postFormDataSnippet() throws Exception { - // tag::request-parameters-form-data[] - this.mockMvc.perform(post("/users").param("username", "Tester")) // <1> - .andExpect(status().isCreated()) - .andDo(document("create-user", requestParameters( - parameterWithName("username").description("The user's username") - ))); - // end::request-parameters-form-data[] + // end::query-parameters[] } } diff --git a/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java new file mode 100644 index 000000000..201ad3b41 --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java @@ -0,0 +1,59 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package com.example.mockmvc; + +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class RequestPartPayload { + + private MockMvc mockMvc; + + public void fields() throws Exception { + // tag::fields[] + MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", "<>".getBytes()); + MockMultipartFile metadata = new MockMultipartFile("metadata", "", "application/json", + "{ \"version\": \"1.0\"}".getBytes()); + + this.mockMvc.perform(multipart("/images").file(image).file(metadata).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("image-upload", requestPartFields("metadata", // <1> + fieldWithPath("version").description("The version of the image")))); // <2> + // end::fields[] + } + + public void body() throws Exception { + // tag::body[] + MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", "<>".getBytes()); + MockMultipartFile metadata = new MockMultipartFile("metadata", "", "application/json", + "{ \"version\": \"1.0\"}".getBytes()); + + this.mockMvc.perform(multipart("/images").file(image).file(metadata).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("image-upload", requestPartBody("metadata"))); // <1> + // end::body[] + } + +} diff --git a/docs/src/test/java/com/example/mockmvc/RequestParts.java b/docs/src/test/java/com/example/mockmvc/RequestParts.java index ba4c6806e..19f0cb0e8 100644 --- a/docs/src/test/java/com/example/mockmvc/RequestParts.java +++ b/docs/src/test/java/com/example/mockmvc/RequestParts.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class RequestParts { @@ -30,11 +30,11 @@ public class RequestParts { public void upload() throws Exception { // tag::request-parts[] - this.mockMvc.perform(fileUpload("/upload").file("file", "example".getBytes())) // <1> + this.mockMvc.perform(multipart("/upload").file("file", "example".getBytes())) // <1> .andExpect(status().isOk()) - .andDo(document("upload", requestParts( // <2> + .andDo(document("upload", requestParts(// <2> partWithName("file").description("The file to upload")) // <3> - )); + )); // end::request-parts[] } diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java b/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java new file mode 100644 index 000000000..2d0fa772d --- /dev/null +++ b/docs/src/test/java/com/example/restassured/CustomDefaultOperationPreprocessors.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package com.example.restassured; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +@ExtendWith(RestDocumentationExtension.class) +class CustomDefaultOperationPreprocessors { + + @SuppressWarnings("unused") + private RequestSpecification spec; + + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { + // tag::custom-default-operation-preprocessors[] + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(restDocumentation).operationPreprocessors() + .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> + .withResponseDefaults(prettyPrint())) // <2> + .build(); + // end::custom-default-operation-preprocessors[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index 7950595cc..3b6d175f1 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,32 +16,29 @@ package com.example.restassured; -import com.jayway.restassured.builder.RequestSpecBuilder; -import com.jayway.restassured.specification.RequestSpecification; -import org.junit.Before; -import org.junit.Rule; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -public class CustomDefaultSnippets { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build"); +@ExtendWith(RestDocumentationExtension.class) +class CustomDefaultSnippets { @SuppressWarnings("unused") private RequestSpecification spec; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-default-snippets[] this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation).snippets() - .withDefaults(curlRequest())) - .build(); + .addFilter(documentationConfiguration(restDocumentation).snippets().withDefaults(curlRequest())) + .build(); // end::custom-default-snippets[] } diff --git a/docs/src/test/java/com/example/restassured/CustomEncoding.java b/docs/src/test/java/com/example/restassured/CustomEncoding.java index 27f09cc31..316b19a21 100644 --- a/docs/src/test/java/com/example/restassured/CustomEncoding.java +++ b/docs/src/test/java/com/example/restassured/CustomEncoding.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,30 +16,28 @@ package com.example.restassured; -import org.junit.Before; -import org.junit.Rule; -import org.springframework.restdocs.JUnitRestDocumentation; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import com.jayway.restassured.builder.RequestSpecBuilder; -import com.jayway.restassured.specification.RequestSpecification; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -public class CustomEncoding { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); +@ExtendWith(RestDocumentationExtension.class) +class CustomEncoding { @SuppressWarnings("unused") private RequestSpecification spec; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-encoding[] this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation) - .snippets().withEncoding("ISO-8859-1")) - .build(); + .addFilter(documentationConfiguration(restDocumentation).snippets().withEncoding("ISO-8859-1")) + .build(); // end::custom-encoding[] } diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java index e1fab2bdb..5f690f1b3 100644 --- a/docs/src/test/java/com/example/restassured/CustomFormat.java +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,31 +16,30 @@ package com.example.restassured; -import org.junit.Before; -import org.junit.Rule; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.templates.TemplateFormats; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import com.jayway.restassured.builder.RequestSpecBuilder; -import com.jayway.restassured.specification.RequestSpecification; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.templates.TemplateFormats; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -public class CustomFormat { +@ExtendWith(RestDocumentationExtension.class) +class CustomFormat { - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); - @SuppressWarnings("unused") private RequestSpecification spec; - @Before - public void setUp() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { // tag::custom-format[] this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation) - .snippets().withTemplateFormat(TemplateFormats.markdown())) - .build(); + .addFilter(documentationConfiguration(restDocumentation).snippets() + .withTemplateFormat(TemplateFormats.markdown())) + .build(); // end::custom-format[] } diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java index b8474742b..741cfdfe9 100644 --- a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,56 +16,48 @@ package com.example.restassured; -import org.junit.Before; -import org.junit.Rule; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.restassured.RestDocumentationFilter; +import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import com.jayway.restassured.RestAssured; -import com.jayway.restassured.builder.RequestSpecBuilder; -import com.jayway.restassured.specification.RequestSpecification; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -public class EveryTestPreprocessing { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "target/generated-snippets"); +@ExtendWith(RestDocumentationExtension.class) +class EveryTestPreprocessing { // tag::setup[] private RequestSpecification spec; - private RestDocumentationFilter documentationFilter; - - @Before - public void setup() { - this.documentationFilter = document("{method-name}", - preprocessRequest(removeHeaders("Foo")), - preprocessResponse(prettyPrint())); // <1> + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation)) - .addFilter(this.documentationFilter)// <2> - .build(); + .addFilter(documentationConfiguration(restDocumentation).operationPreprocessors() + .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> + .withResponseDefaults(prettyPrint())) // <2> + .build(); } - // end::setup[] - public void use() throws Exception { + void use() { // tag::use[] - RestAssured.given(this.spec) // <1> - .filter(this.documentationFilter.document( // <2> - links(linkWithRel("self").description("Canonical self link")))) - .when().get("/") - .then().assertThat().statusCode(is(200)); + RestAssured.given(this.spec) + .filter(document("index", links(linkWithRel("self").description("Canonical self link")))) + .when() + .get("/") + .then() + .assertThat() + .statusCode(is(200)); // end::use[] } diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java index 980446cfa..d1e07b215 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,18 @@ import java.lang.reflect.Method; -import org.springframework.restdocs.ManualRestDocumentation; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; -import com.jayway.restassured.builder.RequestSpecBuilder; -import com.jayway.restassured.specification.RequestSpecification; +import org.springframework.restdocs.ManualRestDocumentation; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class ExampleApplicationTestNgTests { - private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( - "build/generated-snippets"); + private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); @SuppressWarnings("unused") // tag::setup[] @@ -38,9 +37,7 @@ public class ExampleApplicationTestNgTests { @BeforeMethod public void setUp(Method method) { - this.spec = new RequestSpecBuilder().addFilter( - documentationConfiguration(this.restDocumentation)) - .build(); + this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(this.restDocumentation)).build(); this.restDocumentation.beforeTest(getClass(), method.getName()); } @@ -52,4 +49,5 @@ public void tearDown() { this.restDocumentation.afterTest(); } // end::teardown[] + } diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java index 8222ac261..7affb1ef2 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,30 +16,28 @@ package com.example.restassured; -import org.junit.Before; -import org.junit.Rule; -import org.springframework.restdocs.JUnitRestDocumentation; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import com.jayway.restassured.builder.RequestSpecBuilder; -import com.jayway.restassured.specification.RequestSpecification; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -public class ExampleApplicationTests { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build/generated-snippets"); +@ExtendWith(RestDocumentationExtension.class) +class ExampleApplicationTests { @SuppressWarnings("unused") // tag::setup[] private RequestSpecification spec; - @Before - public void setUp() { - this.spec = new RequestSpecBuilder().addFilter( - documentationConfiguration(this.restDocumentation)) // <1> - .build(); + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(restDocumentation)) // <1> + .build(); } // end::setup[] + } diff --git a/docs/src/test/java/com/example/restassured/FormParameters.java b/docs/src/test/java/com/example/restassured/FormParameters.java new file mode 100644 index 000000000..88a720f14 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/FormParameters.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package com.example.restassured; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.request.RequestDocumentation.formParameters; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class FormParameters { + + private RequestSpecification spec; + + public void postFormDataSnippet() { + // tag::form-parameters[] + RestAssured.given(this.spec) + .filter(document("create-user", formParameters(// <1> + parameterWithName("username").description("The user's username")))) // <2> + .formParam("username", "Tester") + .when() + .post("/users") // <3> + .then() + .assertThat() + .statusCode(is(200)); + // end::form-parameters[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/HttpCookies.java b/docs/src/test/java/com/example/restassured/HttpCookies.java new file mode 100644 index 000000000..3ad4993ca --- /dev/null +++ b/docs/src/test/java/com/example/restassured/HttpCookies.java @@ -0,0 +1,49 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package com.example.restassured; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class HttpCookies { + + private RequestSpecification spec; + + public void cookies() { + // tag::cookies[] + RestAssured.given(this.spec) + .filter(document("cookies", requestCookies(// <1> + cookieWithName("JSESSIONID").description("Saved session token")), // <2> + responseCookies(// <3> + cookieWithName("logged_in").description("If user is logged in"), + cookieWithName("JSESSIONID").description("Updated session token")))) + .cookie("JSESSIONID", "ACBCDFD0FF93D5BB") // <4> + .when() + .get("/people") + .then() + .assertThat() + .statusCode(is(200)); + // end::cookies[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/HttpHeaders.java b/docs/src/test/java/com/example/restassured/HttpHeaders.java index fff0fc3df..4595c351c 100644 --- a/docs/src/test/java/com/example/restassured/HttpHeaders.java +++ b/docs/src/test/java/com/example/restassured/HttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package com.example.restassured; -import com.jayway.restassured.RestAssured; -import com.jayway.restassured.specification.RequestSpecification; +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; @@ -29,23 +29,25 @@ public class HttpHeaders { private RequestSpecification spec; - public void headers() throws Exception { + public void headers() { // tag::headers[] RestAssured.given(this.spec) - .filter(document("headers", - requestHeaders( // <1> - headerWithName("Authorization").description( - "Basic auth credentials")), // <2> - responseHeaders( // <3> - headerWithName("X-RateLimit-Limit").description( - "The total number of requests permitted per period"), - headerWithName("X-RateLimit-Remaining").description( - "Remaining requests permitted in current period"), - headerWithName("X-RateLimit-Reset").description( - "Time at which the rate limit period will reset")))) - .header("Authroization", "Basic dXNlcjpzZWNyZXQ=") // <4> - .when().get("/people") - .then().assertThat().statusCode(is(200)); + .filter(document("headers", requestHeaders(// <1> + headerWithName("Authorization").description("Basic auth credentials")), // <2> + responseHeaders(// <3> + headerWithName("X-RateLimit-Limit") + .description("The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining") + .description("Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset") + .description("Time at which the rate limit period will reset")))) + .header("Authorization", "Basic dXNlcjpzZWNyZXQ=") // <4> + .when() + .get("/people") + .then() + .assertThat() + .statusCode(is(200)); // end::headers[] } + } diff --git a/docs/src/test/java/com/example/restassured/Hypermedia.java b/docs/src/test/java/com/example/restassured/Hypermedia.java index 05937ee08..410daca80 100644 --- a/docs/src/test/java/com/example/restassured/Hypermedia.java +++ b/docs/src/test/java/com/example/restassured/Hypermedia.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package com.example.restassured; -import com.jayway.restassured.RestAssured; -import com.jayway.restassured.specification.RequestSpecification; +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks; @@ -29,26 +29,32 @@ public class Hypermedia { private RequestSpecification spec; - public void defaultExtractor() throws Exception { + public void defaultExtractor() { // tag::links[] RestAssured.given(this.spec) .accept("application/json") - .filter(document("index", links( // <1> + .filter(document("index", links(// <1> linkWithRel("alpha").description("Link to the alpha resource"), // <2> linkWithRel("bravo").description("Link to the bravo resource")))) // <3> - .get("/").then().assertThat().statusCode(is(200)); + .get("/") + .then() + .assertThat() + .statusCode(is(200)); // end::links[] } - public void explicitExtractor() throws Exception { + public void explicitExtractor() { RestAssured.given(this.spec) - .accept("application/json") - // tag::explicit-extractor[] - .filter(document("index", links(halLinks(), // <1> - linkWithRel("alpha").description("Link to the alpha resource"), - linkWithRel("bravo").description("Link to the bravo resource")))) - // end::explicit-extractor[] - .get("/").then().assertThat().statusCode(is(200)); + .accept("application/json") + // tag::explicit-extractor[] + .filter(document("index", links(halLinks(), // <1> + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))) + // end::explicit-extractor[] + .get("/") + .then() + .assertThat() + .statusCode(is(200)); } } diff --git a/docs/src/test/java/com/example/restassured/InvokeService.java b/docs/src/test/java/com/example/restassured/InvokeService.java index 8118663d9..8d5de1b7d 100644 --- a/docs/src/test/java/com/example/restassured/InvokeService.java +++ b/docs/src/test/java/com/example/restassured/InvokeService.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package com.example.restassured; -import com.jayway.restassured.RestAssured; -import com.jayway.restassured.specification.RequestSpecification; +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; @@ -26,13 +26,17 @@ public class InvokeService { private RequestSpecification spec; - public void invokeService() throws Exception { + public void invokeService() { // tag::invoke-service[] RestAssured.given(this.spec) // <1> - .accept("application/json") // <2> - .filter(document("index")) // <3> - .when().get("/") // <4> - .then().assertThat().statusCode(is(200)); // <5> + .accept("application/json") // <2> + .filter(document("index")) // <3> + .when() + .get("/") // <4> + .then() + .assertThat() + .statusCode(is(200)); // <5> // end::invoke-service[] } + } diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java index 591f42fab..7471d1772 100644 --- a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,31 +16,29 @@ package com.example.restassured; -import org.junit.Before; -import org.junit.Rule; -import org.springframework.restdocs.JUnitRestDocumentation; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; -import com.jayway.restassured.builder.RequestSpecBuilder; -import com.jayway.restassured.specification.RequestSpecification; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; +@ExtendWith(RestDocumentationExtension.class) public class ParameterizedOutput { - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build/generated-snippets"); - @SuppressWarnings("unused") private RequestSpecification spec; // tag::parameterized-output[] - @Before - public void setUp() { - this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation)) - .addFilter(document("{method-name}/{step}")).build(); + @BeforeEach + public void setUp(RestDocumentationContextProvider restDocumentation) { + this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(restDocumentation)) + .addFilter(document("{method-name}/{step}")) + .build(); } // end::parameterized-output[] diff --git a/docs/src/test/java/com/example/restassured/PathParameters.java b/docs/src/test/java/com/example/restassured/PathParameters.java index 4ea6de7f5..dc2f3306b 100644 --- a/docs/src/test/java/com/example/restassured/PathParameters.java +++ b/docs/src/test/java/com/example/restassured/PathParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package com.example.restassured; -import com.jayway.restassured.RestAssured; -import com.jayway.restassured.specification.RequestSpecification; +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; @@ -28,14 +28,18 @@ public class PathParameters { private RequestSpecification spec; - public void pathParametersSnippet() throws Exception { + public void pathParametersSnippet() { // tag::path-parameters[] RestAssured.given(this.spec) - .filter(document("locations", pathParameters( // <1> + .filter(document("locations", pathParameters(// <1> parameterWithName("latitude").description("The location's latitude"), // <2> parameterWithName("longitude").description("The location's longitude")))) // <3> - .when().get("/locations/{latitude}/{longitude}", 51.5072, 0.1275) // <4> - .then().assertThat().statusCode(is(200)); + .when() + .get("/locations/{latitude}/{longitude}", 51.5072, 0.1275) // <4> + .then() + .assertThat() + .statusCode(is(200)); // end::path-parameters[] } + } diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index d02737b72..6c90f2299 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,19 @@ package com.example.restassured; +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; -import com.jayway.restassured.RestAssured; -import com.jayway.restassured.specification.RequestSpecification; - import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -34,65 +37,120 @@ public class Payload { private RequestSpecification spec; - public void response() throws Exception { + public void response() { // tag::response[] - RestAssured.given(this.spec).accept("application/json") - .filter(document("user", responseFields( // <1> - fieldWithPath("contact").description("The user's contact details"), // <2> + RestAssured.given(this.spec) + .accept("application/json") + .filter(document("user", responseFields(// <1> + fieldWithPath("contact.name").description("The user's name"), // <2> fieldWithPath("contact.email").description("The user's email address")))) // <3> - .when().get("/user/5") - .then().assertThat().statusCode(is(200)); + .when() + .get("/user/5") + .then() + .assertThat() + .statusCode(is(200)); // end::response[] } - public void explicitType() throws Exception { - RestAssured.given(this.spec).accept("application/json") + public void subsection() { + // tag::subsection[] + RestAssured.given(this.spec) + .accept("application/json") + .filter(document("user", + responseFields(subsectionWithPath("contact").description("The user's contact details")))) // <1> + .when() + .get("/user/5") + .then() + .assertThat() + .statusCode(is(200)); + // end::subsection[] + } + + public void explicitType() { + RestAssured.given(this.spec) + .accept("application/json") // tag::explicit-type[] - .filter(document("user", responseFields( - fieldWithPath("contact.email") - .type(JsonFieldType.STRING) // <1> - .description("The user's email address")))) + .filter(document("user", responseFields(fieldWithPath("contact.email").type(JsonFieldType.STRING) // <1> + .description("The user's email address")))) // end::explicit-type[] - .when().get("/user/5") - .then().assertThat().statusCode(is(200)); + .when() + .get("/user/5") + .then() + .assertThat() + .statusCode(is(200)); } - public void constraints() throws Exception { - RestAssured.given(this.spec).accept("application/json") + public void constraints() { + RestAssured.given(this.spec) + .accept("application/json") // tag::constraints[] - .filter(document("create-user", requestFields( - attributes(key("title").value("Fields for user creation")), // <1> + .filter(document("create-user", requestFields(attributes(key("title").value("Fields for user creation")), // <1> fieldWithPath("name").description("The user's name") - .attributes(key("constraints") - .value("Must not be null. Must not be empty")), // <2> + .attributes(key("constraints").value("Must not be null. Must not be empty")), // <2> fieldWithPath("email").description("The user's email address") - .attributes(key("constraints") - .value("Must be a valid email address"))))) // <3> + .attributes(key("constraints").value("Must be a valid email address"))))) // <3> // end::constraints[] - .when().post("/users") - .then().assertThat().statusCode(is(200)); + .when() + .post("/users") + .then() + .assertThat() + .statusCode(is(200)); } - public void descriptorReuse() throws Exception { - FieldDescriptor[] book = new FieldDescriptor[] { - fieldWithPath("title").description("Title of the book"), + public void descriptorReuse() { + FieldDescriptor[] book = new FieldDescriptor[] { fieldWithPath("title").description("Title of the book"), fieldWithPath("author").description("Author of the book") }; // tag::single-book[] - RestAssured.given(this.spec).accept("application/json") + RestAssured.given(this.spec) + .accept("application/json") .filter(document("book", responseFields(book))) // <1> - .when().get("/books/1") - .then().assertThat().statusCode(is(200)); + .when() + .get("/books/1") + .then() + .assertThat() + .statusCode(is(200)); // end::single-book[] // tag::book-array[] - RestAssured.given(this.spec).accept("application/json") - .filter(document("books", responseFields( - fieldWithPath("[]").description("An array of books")) // <1> + RestAssured.given(this.spec) + .accept("application/json") + .filter(document("books", responseFields(fieldWithPath("[]").description("An array of books")) // <1> .andWithPrefix("[].", book))) // <2> - .when().get("/books") - .then().assertThat().statusCode(is(200)); + .when() + .get("/books") + .then() + .assertThat() + .statusCode(is(200)); // end::book-array[] } + public void fieldsSubsection() { + // tag::fields-subsection[] + RestAssured.given(this.spec) + .accept("application/json") + .filter(document("location", responseFields(beneathPath("weather.temperature"), // <1> + fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> + fieldWithPath("low").description("The forecast low in degrees celcius")))) + .when() + .get("/locations/1") + .then() + .assertThat() + .statusCode(is(200)); + // end::fields-subsection[] + } + + public void bodySubsection() { + // tag::body-subsection[] + RestAssured.given(this.spec) + .accept("application/json") + .filter(document("location", responseBody(beneathPath("weather.temperature")))) // <1> + .when() + .get("/locations/1") + .then() + .assertThat() + .statusCode(is(200)); + // end::body-subsection[] + } + } diff --git a/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java index d15b52f78..eeb937af5 100644 --- a/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,29 +16,31 @@ package com.example.restassured; -import com.jayway.restassured.RestAssured; -import com.jayway.restassured.specification.RequestSpecification; +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; - +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class PerTestPreprocessing { private RequestSpecification spec; - public void general() throws Exception { + public void general() { // tag::preprocessing[] RestAssured.given(this.spec) - .filter(document("index", preprocessRequest(removeHeaders("Foo")), // <1> + .filter(document("index", preprocessRequest(modifyHeaders().remove("Foo")), // <1> preprocessResponse(prettyPrint()))) // <2> - .when().get("/") - .then().assertThat().statusCode(is(200)); - // end::preprocessing[] + .when() + .get("/") + .then() + .assertThat() + .statusCode(is(200)); + // end::preprocessing[] } } diff --git a/docs/src/test/java/com/example/restassured/RequestParameters.java b/docs/src/test/java/com/example/restassured/QueryParameters.java similarity index 54% rename from docs/src/test/java/com/example/restassured/RequestParameters.java rename to docs/src/test/java/com/example/restassured/QueryParameters.java index 39cac4489..27488ed93 100644 --- a/docs/src/test/java/com/example/restassured/RequestParameters.java +++ b/docs/src/test/java/com/example/restassured/QueryParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,38 +16,30 @@ package com.example.restassured; -import com.jayway.restassured.RestAssured; -import com.jayway.restassured.specification.RequestSpecification; +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; -public class RequestParameters { +public class QueryParameters { private RequestSpecification spec; - public void getQueryStringSnippet() throws Exception { - // tag::request-parameters-query-string[] + public void getQueryStringSnippet() { + // tag::query-parameters[] RestAssured.given(this.spec) - .filter(document("users", requestParameters( // <1> + .filter(document("users", queryParameters(// <1> parameterWithName("page").description("The page to retrieve"), // <2> parameterWithName("per_page").description("Entries per page")))) // <3> - .when().get("/users?page=2&per_page=100") // <4> - .then().assertThat().statusCode(is(200)); - // end::request-parameters-query-string[] - } - - public void postFormDataSnippet() throws Exception { - // tag::request-parameters-form-data[] - RestAssured.given(this.spec) - .filter(document("create-user", requestParameters( - parameterWithName("username").description("The user's username")))) - .formParam("username", "Tester") // <1> - .when().post("/users") // <2> - .then().assertThat().statusCode(is(200)); - // end::request-parameters-form-data[] + .when() + .get("/users?page=2&per_page=100") // <4> + .then() + .assertThat() + .statusCode(is(200)); + // end::query-parameters[] } } diff --git a/docs/src/test/java/com/example/restassured/RequestPartPayload.java b/docs/src/test/java/com/example/restassured/RequestPartPayload.java new file mode 100644 index 000000000..2566c37a9 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RequestPartPayload.java @@ -0,0 +1,71 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package com.example.restassured; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RequestPartPayload { + + private RequestSpecification spec; + + public void fields() { + // tag::fields[] + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + RestAssured.given(this.spec) + .accept("application/json") + .filter(document("image-upload", requestPartFields("metadata", // <1> + fieldWithPath("version").description("The version of the image")))) // <2> + .when() + .multiPart("image", new File("image.png"), "image/png") + .multiPart("metadata", metadata) + .post("images") + .then() + .assertThat() + .statusCode(is(200)); + // end::fields[] + } + + public void body() { + // tag::body[] + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + RestAssured.given(this.spec) + .accept("application/json") + .filter(document("image-upload", requestPartBody("metadata"))) // <1> + .when() + .multiPart("image", new File("image.png"), "image/png") + .multiPart("metadata", metadata) + .post("images") + .then() + .assertThat() + .statusCode(is(200)); + // end::body[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RequestParts.java b/docs/src/test/java/com/example/restassured/RequestParts.java index fefe818ab..ee3547367 100644 --- a/docs/src/test/java/com/example/restassured/RequestParts.java +++ b/docs/src/test/java/com/example/restassured/RequestParts.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package com.example.restassured; -import com.jayway.restassured.RestAssured; -import com.jayway.restassured.specification.RequestSpecification; +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; @@ -28,14 +28,16 @@ public class RequestParts { private RequestSpecification spec; - public void upload() throws Exception { + public void upload() { // tag::request-parts[] RestAssured.given(this.spec) - .filter(document("users", requestParts( // <1> + .filter(document("users", requestParts(// <1> partWithName("file").description("The file to upload")))) // <2> .multiPart("file", "example") // <3> - .when().post("/upload") // <4> - .then().statusCode(is(200)); + .when() + .post("/upload") // <4> + .then() + .statusCode(is(200)); // end::request-parts[] } diff --git a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java index 2ac37d260..800903a00 100644 --- a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java +++ b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,8 @@ package com.example.restassured; import com.example.SnippetReuse; -import com.jayway.restassured.RestAssured; -import com.jayway.restassured.specification.RequestSpecification; - +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; @@ -29,14 +28,17 @@ public class RestAssuredSnippetReuse extends SnippetReuse { private RequestSpecification spec; - public void documentation() throws Exception { + public void documentation() { // tag::use[] RestAssured.given(this.spec) .accept("application/json") - .filter(document("example", this.pagingLinks.and( // <1> + .filter(document("example", this.pagingLinks.and(// <1> linkWithRel("alpha").description("Link to the alpha resource"), linkWithRel("bravo").description("Link to the bravo resource")))) - .get("/").then().assertThat().statusCode(is(200)); + .get("/") + .then() + .assertThat() + .statusCode(is(200)); // end::use[] } diff --git a/docs/src/test/java/com/example/webtestclient/CustomDefaultOperationPreprocessors.java b/docs/src/test/java/com/example/webtestclient/CustomDefaultOperationPreprocessors.java new file mode 100644 index 000000000..17ee06b44 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/CustomDefaultOperationPreprocessors.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package com.example.webtestclient; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.context.ApplicationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; + +@ExtendWith(RestDocumentationExtension.class) +class CustomDefaultOperationPreprocessors { + + // @formatter:off + + private ApplicationContext context; + + @SuppressWarnings("unused") + private WebTestClient webTestClient; + + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { + // tag::custom-default-operation-preprocessors[] + this.webTestClient = WebTestClient.bindToApplicationContext(this.context) + .configureClient() + .filter(documentationConfiguration(restDocumentation) + .operationPreprocessors() + .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> + .withResponseDefaults(prettyPrint())) // <2> + .build(); + // end::custom-default-operation-preprocessors[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/CustomDefaultSnippets.java b/docs/src/test/java/com/example/webtestclient/CustomDefaultSnippets.java new file mode 100644 index 000000000..420a52f49 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/CustomDefaultSnippets.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package com.example.webtestclient; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; + +@ExtendWith(RestDocumentationExtension.class) +class CustomDefaultSnippets { + + // @formatter:off + + @Autowired + private ApplicationContext context; + + @SuppressWarnings("unused") + private WebTestClient webTestClient; + + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + // tag::custom-default-snippets[] + this.webTestClient = WebTestClient.bindToApplicationContext(this.context) + .configureClient().filter( + documentationConfiguration(restDocumentation) + .snippets().withDefaults(curlRequest())) + .build(); + // end::custom-default-snippets[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/CustomEncoding.java b/docs/src/test/java/com/example/webtestclient/CustomEncoding.java new file mode 100644 index 000000000..42800d797 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/CustomEncoding.java @@ -0,0 +1,51 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package com.example.webtestclient; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; + +@ExtendWith(RestDocumentationExtension.class) +class CustomEncoding { + + // @formatter:off + + @Autowired + private ApplicationContext context; + + @SuppressWarnings("unused") + private WebTestClient webTestClient; + + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + // tag::custom-encoding[] + this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient() + .filter(documentationConfiguration(restDocumentation) + .snippets().withEncoding("ISO-8859-1")) + .build(); + // end::custom-encoding[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/CustomFormat.java b/docs/src/test/java/com/example/webtestclient/CustomFormat.java new file mode 100644 index 000000000..ab57f1846 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/CustomFormat.java @@ -0,0 +1,52 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package com.example.webtestclient; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; + +@ExtendWith(RestDocumentationExtension.class) +class CustomFormat { + + // @formatter:off + + @Autowired + private ApplicationContext context; + + @SuppressWarnings("unused") + private WebTestClient webTestClient; + + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + // tag::custom-format[] + this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient() + .filter(documentationConfiguration(restDocumentation) + .snippets().withTemplateFormat(TemplateFormats.markdown())) + .build(); + // end::custom-format[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/CustomUriConfiguration.java b/docs/src/test/java/com/example/webtestclient/CustomUriConfiguration.java new file mode 100644 index 000000000..9db88cded --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/CustomUriConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package com.example.webtestclient; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; + +@ExtendWith(RestDocumentationExtension.class) +class CustomUriConfiguration { + + @SuppressWarnings("unused") + private WebTestClient webTestClient; + + @Autowired + private ApplicationContext context; + + // tag::custom-uri-configuration[] + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + this.webTestClient = WebTestClient.bindToApplicationContext(this.context) + .configureClient() + .baseUrl("https://api.example.com") // <1> + .filter(documentationConfiguration(restDocumentation)) + .build(); + } + // end::custom-uri-configuration[] + +} diff --git a/docs/src/test/java/com/example/webtestclient/EveryTestPreprocessing.java b/docs/src/test/java/com/example/webtestclient/EveryTestPreprocessing.java new file mode 100644 index 000000000..e6ff7ceb9 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/EveryTestPreprocessing.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package com.example.webtestclient; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.context.ApplicationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; + +@ExtendWith(RestDocumentationExtension.class) +class EveryTestPreprocessing { + + // @formatter:off + + private ApplicationContext context; + + // tag::setup[] + private WebTestClient webTestClient; + + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { + this.webTestClient = WebTestClient.bindToApplicationContext(this.context) + .configureClient() + .filter(documentationConfiguration(restDocumentation) + .operationPreprocessors() + .withRequestDefaults(modifyHeaders().remove("Foo")) // <1> + .withResponseDefaults(prettyPrint())) // <2> + .build(); + } + // end::setup[] + + void use() { + // tag::use[] + this.webTestClient.get().uri("/").exchange().expectStatus().isOk() + .expectBody().consumeWith(document("index", + links(linkWithRel("self").description("Canonical self link")))); + // end::use[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/webtestclient/ExampleApplicationTestNgTests.java new file mode 100644 index 000000000..7e905b0c2 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/ExampleApplicationTestNgTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package com.example.webtestclient; + +import java.lang.reflect.Method; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.restdocs.ManualRestDocumentation; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; + +public class ExampleApplicationTestNgTests { + + public final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); + + @SuppressWarnings("unused") + // tag::setup[] + private WebTestClient webTestClient; + + @Autowired + private ApplicationContext context; + + @BeforeMethod + public void setUp(Method method) { + this.webTestClient = WebTestClient.bindToApplicationContext(this.context) + .configureClient() + .filter(documentationConfiguration(this.restDocumentation)) // <1> + .build(); + this.restDocumentation.beforeTest(getClass(), method.getName()); + } + + // end::setup[] + + // tag::teardown[] + @AfterMethod + public void tearDown() { + this.restDocumentation.afterTest(); + } + // end::teardown[] + +} diff --git a/docs/src/test/java/com/example/webtestclient/ExampleApplicationTests.java b/docs/src/test/java/com/example/webtestclient/ExampleApplicationTests.java new file mode 100644 index 000000000..3126487ad --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/ExampleApplicationTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package com.example.webtestclient; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.context.ApplicationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; + +@ExtendWith(RestDocumentationExtension.class) +class ExampleApplicationTests { + + @SuppressWarnings("unused") + // tag::setup[] + private WebTestClient webTestClient; + + @BeforeEach + void setUp(ApplicationContext applicationContext, RestDocumentationContextProvider restDocumentation) { + this.webTestClient = WebTestClient.bindToApplicationContext(applicationContext) + .configureClient() + .filter(documentationConfiguration(restDocumentation)) // <1> + .build(); + } + // end::setup[] + +} diff --git a/docs/src/test/java/com/example/webtestclient/FormParameters.java b/docs/src/test/java/com/example/webtestclient/FormParameters.java new file mode 100644 index 000000000..0bfdbc0c1 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/FormParameters.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package com.example.webtestclient; + +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; + +import static org.springframework.restdocs.request.RequestDocumentation.formParameters; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class FormParameters { + + // @formatter:off + + private WebTestClient webTestClient; + + public void postFormDataSnippet() { + // tag::form-parameters[] + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("username", "Tester"); + this.webTestClient.post().uri("/users").body(BodyInserters.fromFormData(formData)) // <1> + .exchange().expectStatus().isCreated().expectBody() + .consumeWith(document("create-user", formParameters(// <2> + parameterWithName("username").description("The user's username") // <3> + ))); + // end::form-parameters[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/HttpCookies.java b/docs/src/test/java/com/example/webtestclient/HttpCookies.java new file mode 100644 index 000000000..1db0fe475 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/HttpCookies.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package com.example.webtestclient; + +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class HttpCookies { + + private WebTestClient webTestClient; + + public void cookies() { + // tag::cookies[] + this.webTestClient.get() + .uri("/people") + .cookie("JSESSIONID", "ACBCDFD0FF93D5BB=") // <1> + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith(document("cookies", requestCookies(// <2> + cookieWithName("JSESSIONID").description("Session token")), // <3> + responseCookies(// <4> + cookieWithName("JSESSIONID").description("Updated session token"), + cookieWithName("logged_in").description("User is logged in")))); + // end::cookies[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/HttpHeaders.java b/docs/src/test/java/com/example/webtestclient/HttpHeaders.java new file mode 100644 index 000000000..9e0041851 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/HttpHeaders.java @@ -0,0 +1,49 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package com.example.webtestclient; + +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class HttpHeaders { + + // @formatter:off + + private WebTestClient webTestClient; + + public void headers() { + // tag::headers[] + this.webTestClient + .get().uri("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=") // <1> + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("headers", + requestHeaders(// <2> + headerWithName("Authorization").description("Basic auth credentials")), // <3> + responseHeaders(// <4> + headerWithName("X-RateLimit-Limit") + .description("The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining") + .description("Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset") + .description("Time at which the rate limit period will reset")))); + // end::headers[] + } +} diff --git a/docs/src/test/java/com/example/webtestclient/Hypermedia.java b/docs/src/test/java/com/example/webtestclient/Hypermedia.java new file mode 100644 index 000000000..93b79ed5a --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/Hypermedia.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package com.example.webtestclient; + +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class Hypermedia { + + private WebTestClient webTestClient; + + public void defaultExtractor() { + // tag::links[] + this.webTestClient.get() + .uri("/") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith(document("index", links(// <1> + linkWithRel("alpha").description("Link to the alpha resource"), // <2> + linkWithRel("bravo").description("Link to the bravo resource")))); // <3> + // end::links[] + } + + public void explicitExtractor() { + this.webTestClient.get() + .uri("/") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isOk() + .expectBody() + // tag::explicit-extractor[] + .consumeWith(document("index", links(halLinks(), // <1> + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))); + // end::explicit-extractor[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/InvokeService.java b/docs/src/test/java/com/example/webtestclient/InvokeService.java new file mode 100644 index 000000000..20859f27a --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/InvokeService.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package com.example.webtestclient; + +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class InvokeService { + + private WebTestClient webTestClient; + + public void invokeService() { + // tag::invoke-service[] + this.webTestClient.get() + .uri("/") + .accept(MediaType.APPLICATION_JSON) // <1> + .exchange() + .expectStatus() + .isOk() // <2> + .expectBody() + .consumeWith(document("index")); // <3> + // end::invoke-service[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/ParameterizedOutput.java b/docs/src/test/java/com/example/webtestclient/ParameterizedOutput.java new file mode 100644 index 000000000..f37702746 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/ParameterizedOutput.java @@ -0,0 +1,51 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package com.example.webtestclient; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; + +@ExtendWith(RestDocumentationExtension.class) +class ParameterizedOutput { + + @SuppressWarnings("unused") + private WebTestClient webTestClient; + + @Autowired + private ApplicationContext context; + + // tag::parameterized-output[] + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + this.webTestClient = WebTestClient.bindToApplicationContext(this.context) + .configureClient() + .filter(documentationConfiguration(restDocumentation)) + .entityExchangeResultConsumer(document("{method-name}/{step}")) + .build(); + } + // end::parameterized-output[] + +} diff --git a/docs/src/test/java/com/example/webtestclient/PathParameters.java b/docs/src/test/java/com/example/webtestclient/PathParameters.java new file mode 100644 index 000000000..5199f6802 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/PathParameters.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package com.example.webtestclient; + +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class PathParameters { + + // @formatter:off + + private WebTestClient webTestClient; + + public void pathParametersSnippet() { + // tag::path-parameters[] + this.webTestClient.get().uri("/locations/{latitude}/{longitude}", 51.5072, 0.1275) // <1> + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("locations", + pathParameters(// <2> + parameterWithName("latitude").description("The location's latitude"), // <3> + parameterWithName("longitude").description("The location's longitude")))); // <4> + // end::path-parameters[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/Payload.java b/docs/src/test/java/com/example/webtestclient/Payload.java new file mode 100644 index 000000000..d7d828e57 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/Payload.java @@ -0,0 +1,132 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package com.example.webtestclient; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class Payload { + + // @formatter:off + + private WebTestClient webTestClient; + + public void response() { + // tag::response[] + this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("user", + responseFields(// <1> + fieldWithPath("contact.email").description("The user's email address"), // <2> + fieldWithPath("contact.name").description("The user's name")))); // <3> + // end::response[] + } + + public void subsection() { + // tag::subsection[] + this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("user", + responseFields( + subsectionWithPath("contact").description("The user's contact details")))); // <1> + // end::subsection[] + } + + public void explicitType() { + this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + // tag::explicit-type[] + .consumeWith(document("user", + responseFields( + fieldWithPath("contact.email") + .type(JsonFieldType.STRING) // <1> + .description("The user's email address")))); + // end::explicit-type[] + } + + public void constraints() { + this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + // tag::constraints[] + .consumeWith(document("create-user", + requestFields( + attributes(key("title").value("Fields for user creation")), // <1> + fieldWithPath("name") + .description("The user's name") + .attributes(key("constraints").value("Must not be null. Must not be empty")), // <2> + fieldWithPath("email") + .description("The user's email address") + .attributes(key("constraints").value("Must be a valid email address"))))); // <3> + // end::constraints[] + } + + public void descriptorReuse() { + FieldDescriptor[] book = new FieldDescriptor[] { + fieldWithPath("title").description("Title of the book"), + fieldWithPath("author").description("Author of the book") }; + + // tag::single-book[] + this.webTestClient.get().uri("/books/1").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("book", + responseFields(book))); // <1> + // end::single-book[] + + // tag::book-array[] + this.webTestClient.get().uri("/books").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("books", + responseFields( + fieldWithPath("[]") + .description("An array of books")) // <1> + .andWithPrefix("[].", book))); // <2> + // end::book-array[] + } + + public void fieldsSubsection() { + // tag::fields-subsection[] + this.webTestClient.get().uri("/locations/1").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("temperature", + responseFields(beneathPath("weather.temperature"), // <1> + fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> + fieldWithPath("low").description("The forecast low in degrees celcius")))); + // end::fields-subsection[] + } + + public void bodySubsection() { + // tag::body-subsection[] + this.webTestClient.get().uri("/locations/1").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("temperature", + responseBody(beneathPath("weather.temperature")))); // <1> + // end::body-subsection[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/PerTestPreprocessing.java b/docs/src/test/java/com/example/webtestclient/PerTestPreprocessing.java new file mode 100644 index 000000000..cd1254efe --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/PerTestPreprocessing.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package com.example.webtestclient; + +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class PerTestPreprocessing { + + // @formatter:off + + private WebTestClient webTestClient; + + public void general() { + // tag::preprocessing[] + this.webTestClient.get().uri("/").exchange().expectStatus().isOk().expectBody() + .consumeWith(document("index", + preprocessRequest(modifyHeaders().remove("Foo")), // <1> + preprocessResponse(prettyPrint()))); // <2> + // end::preprocessing[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/QueryParameters.java b/docs/src/test/java/com/example/webtestclient/QueryParameters.java new file mode 100644 index 000000000..347c700b6 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/QueryParameters.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package com.example.webtestclient; + +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class QueryParameters { + + // @formatter:off + + private WebTestClient webTestClient; + + public void getQueryStringSnippet() { + // tag::query-parameters[] + this.webTestClient.get().uri("/users?page=2&per_page=100") // <1> + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("users", queryParameters(// <2> + parameterWithName("page").description("The page to retrieve"), // <3> + parameterWithName("per_page").description("Entries per page") // <4> + ))); + // end::query-parameters[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/RequestPartPayload.java b/docs/src/test/java/com/example/webtestclient/RequestPartPayload.java new file mode 100644 index 000000000..e88126271 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/RequestPartPayload.java @@ -0,0 +1,84 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package com.example.webtestclient; + +import java.util.Collections; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class RequestPartPayload { + + // @formatter:off + + private WebTestClient webTestClient; + + public void fields() { + // tag::fields[] + MultiValueMap multipartData = new LinkedMultiValueMap<>(); + Resource imageResource = new ByteArrayResource("<>".getBytes()) { + + @Override + public String getFilename() { + return "image.png"; + } + + }; + multipartData.add("image", imageResource); + multipartData.add("metadata", Collections.singletonMap("version", "1.0")); + this.webTestClient.post().uri("/images").body(BodyInserters.fromMultipartData(multipartData)) + .accept(MediaType.APPLICATION_JSON).exchange() + .expectStatus().isOk().expectBody() + .consumeWith(document("image-upload", + requestPartFields("metadata", // <1> + fieldWithPath("version").description("The version of the image")))); // <2> + // end::fields[] + } + + public void body() { + // tag::body[] + MultiValueMap multipartData = new LinkedMultiValueMap<>(); + Resource imageResource = new ByteArrayResource("<>".getBytes()) { + + @Override + public String getFilename() { + return "image.png"; + } + + }; + multipartData.add("image", imageResource); + multipartData.add("metadata", Collections.singletonMap("version", "1.0")); + + this.webTestClient.post().uri("/images").body(BodyInserters.fromMultipartData(multipartData)) + .accept(MediaType.APPLICATION_JSON).exchange() + .expectStatus().isOk().expectBody() + .consumeWith(document("image-upload", + requestPartBody("metadata"))); // <1> + // end::body[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/RequestParts.java b/docs/src/test/java/com/example/webtestclient/RequestParts.java new file mode 100644 index 000000000..5d65f0910 --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/RequestParts.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package com.example.webtestclient; + +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; + +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class RequestParts { + + // @formatter:off + + private WebTestClient webTestClient; + + public void upload() { + // tag::request-parts[] + MultiValueMap multipartData = new LinkedMultiValueMap<>(); + multipartData.add("file", "example".getBytes()); + this.webTestClient.post().uri("/upload").body(BodyInserters.fromMultipartData(multipartData)) // <1> + .exchange().expectStatus().isOk().expectBody() + .consumeWith(document("upload", requestParts(// <2> + partWithName("file").description("The file to upload")) // <3> + )); + // end::request-parts[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/WebTestClientSnippetReuse.java b/docs/src/test/java/com/example/webtestclient/WebTestClientSnippetReuse.java new file mode 100644 index 000000000..6f57fc91d --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/WebTestClientSnippetReuse.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package com.example.webtestclient; + +import com.example.SnippetReuse; + +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +public class WebTestClientSnippetReuse extends SnippetReuse { + + // @formatter:off + + private WebTestClient webTestClient; + + public void documentation() { + // tag::use[] + this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange() + .expectStatus().isOk().expectBody() + .consumeWith(document("example", this.pagingLinks.and(// <1> + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))); + // end::use[] + } + +} diff --git a/git/hooks/forward-merge b/git/hooks/forward-merge new file mode 100755 index 000000000..a042bb460 --- /dev/null +++ b/git/hooks/forward-merge @@ -0,0 +1,146 @@ +#!/usr/bin/ruby +require 'json' +require 'net/http' +require 'yaml' +require 'logger' + +$log = Logger.new(STDOUT) +$log.level = Logger::WARN + +class ForwardMerge + attr_reader :issue, :milestone, :message, :line + def initialize(issue, milestone, message, line) + @issue = issue + @milestone = milestone + @message = message + @line = line + end +end + +def find_forward_merges(message_file) + + $log.debug "Searching for forward merge" + branch=`git rev-parse -q --abbrev-ref HEAD`.strip + $log.debug "Found #{branch} from git rev-parse --abbrev-ref" + if( branch == "docs-build") then + $log.debug "Skipping docs build" + return nil + end + rev=`git rev-parse -q --verify MERGE_HEAD`.strip + $log.debug "Found #{rev} from git rev-parse" + return nil unless rev + message = File.read(message_file) + forward_merges = [] + message.each_line do |line| + $log.debug "Checking #{line} for message" + match = /^(?:Fixes|Closes) gh-(\d+) in (\d\.\d\.[\dx](?:[\.\-](?:M|RC)\d)?)$/.match(line) + if match then + issue = match[1] + milestone = match[2] + $log.debug "Matched reference to issue #{issue} in milestone #{milestone}" + forward_merges << ForwardMerge.new(issue, milestone, message, line) + end + end + $log.debug "No match in merge message" unless forward_merges + return forward_merges +end + +def get_issue(username, password, repository, number) + $log.debug "Getting issue #{number} from GitHub repository #{repository}" + uri = URI("https://api.github.com/repos/#{repository}/issues/#{number}") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl=true + request = Net::HTTP::Get.new(uri.path) + request.basic_auth(username, password) + response = http.request(request) + $log.debug "Get HTTP response #{response.code}" + return JSON.parse(response.body) unless response.code != '200' + puts "Failed to retrieve issue #{number}: #{response.message}" + exit 1 +end + +def find_milestone(username, password, repository, title) + $log.debug "Finding milestone #{title} from GitHub repository #{repository}" + uri = URI("https://api.github.com/repos/#{repository}/milestones") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl=true + request = Net::HTTP::Get.new(uri.path) + request.basic_auth(username, password) + response = http.request(request) + milestones = JSON.parse(response.body) + if title.end_with?(".x") + prefix = title.delete_suffix('.x') + $log.debug "Finding nearest milestone from candidates starting with #{prefix}" + titles = milestones.map { |milestone| milestone['title'] } + titles = titles.select{ |title| title.start_with?(prefix) unless title.end_with?('.x') || (title.count('.') > 2)} + titles = titles.sort_by { |v| Gem::Version.new(v) } + $log.debug "Considering candidates #{titles}" + if(titles.empty?) + puts "Cannot find nearest milestone for prefix #{title}" + exit 1 + end + title = titles.first + $log.debug "Found nearest milestone #{title}" + end + milestones.each do |milestone| + $log.debug "Considering #{milestone['title']}" + return milestone['number'] if milestone['title'] == title + end + puts "Milestone #{title} not found" + exit 1 +end + +def create_issue(username, password, repository, original, title, labels, milestone, milestone_name, dry_run) + $log.debug "Finding forward-merge issue in GitHub repository #{repository} for '#{title}'" + uri = URI("https://api.github.com/repos/#{repository}/issues") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl=true + request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json') + request.basic_auth(username, password) + request.body = { + title: title, + labels: labels, + milestone: milestone.to_i, + body: "Forward port of issue ##{original} to #{milestone_name}." + }.to_json + if dry_run then + puts "Dry run" + puts "POSTing to #{uri} with body #{request.body}" + return "dry-run" + end + response = JSON.parse(http.request(request).body) + $log.debug "Created new issue #{response['number']}" + return response['number'] +end + +$log.debug "Running forward-merge hook script" +message_file=ARGV[0] + +forward_merges = find_forward_merges(message_file) +exit 0 unless forward_merges + +$log.debug "Loading config from ~/.spring-restdocs/forward-merge.yml" +config = YAML.load_file(File.join(Dir.home, '.spring-restdocs', 'forward-merge.yml')) +username = config['github']['credentials']['username'] +password = config['github']['credentials']['password'] +dry_run = config['dry_run'] + +gradleProperties = IO.read('gradle.properties') +springBuildType = gradleProperties.match(/^spring\.build-type\s?=\s?(.*)$/) +repository = (springBuildType && springBuildType[1] != 'oss') ? "spring-projects/spring-restdocs-#{springBuildType[1]}" : "spring-projects/spring-restdocs"; +$log.debug "Targeting repository #{repository}" + +forward_merges.each do |forward_merge| + existing_issue = get_issue(username, password, repository, forward_merge.issue) + title = existing_issue['title'] + labels = existing_issue['labels'].map { |label| label['name'] } + labels << "status: forward-port" + $log.debug "Processing issue '#{title}'" + + milestone = find_milestone(username, password, repository, forward_merge.milestone) + new_issue_number = create_issue(username, password, repository, forward_merge.issue, title, labels, milestone, forward_merge.milestone, dry_run) + + puts "Created gh-#{new_issue_number} for forward port of gh-#{forward_merge.issue} into #{forward_merge.milestone}" + rewritten_message = forward_merge.message.sub(forward_merge.line, "Closes gh-#{new_issue_number}\n") + File.write(message_file, rewritten_message) +end diff --git a/git/hooks/prepare-forward-merge b/git/hooks/prepare-forward-merge new file mode 100755 index 000000000..fbdb1e194 --- /dev/null +++ b/git/hooks/prepare-forward-merge @@ -0,0 +1,71 @@ +#!/usr/bin/ruby +require 'json' +require 'net/http' +require 'yaml' +require 'logger' + +$main_branch = "4.0.x" + +$log = Logger.new(STDOUT) +$log.level = Logger::WARN + +def get_fixed_issues() + $log.debug "Searching for forward merge" + rev=`git rev-parse -q --verify MERGE_HEAD`.strip + $log.debug "Found #{rev} from git rev-parse" + return nil unless rev + fixed = [] + message = `git log -1 --pretty=%B #{rev}` + message.each_line do |line| + $log.debug "Checking #{line} for message" + fixed << line.strip if /^(?:Fixes|Closes) gh-(\d+)/.match(line) + end + $log.debug "Found fixed issues #{fixed}" + return fixed; +end + +def rewrite_message(message_file, fixed) + current_branch = `git rev-parse --abbrev-ref HEAD`.strip + if current_branch == "main" + current_branch = $main_branch + end + rewritten_message = "" + message = File.read(message_file) + message.each_line do |line| + match = /^Merge.*branch\ '(.*)'(?:\ into\ (.*))?$/.match(line) + if match + from_branch = match[1] + if from_branch.include? "/" + from_branch = from_branch.partition("/").last + end + to_branch = match[2] + $log.debug "Rewriting merge message" + line = "Merge branch '#{from_branch}'" + (to_branch ? " into #{to_branch}\n" : "\n") + end + if fixed and line.start_with?("#") + $log.debug "Adding fixed" + rewritten_message << "\n" + fixed.each do |fixes| + rewritten_message << "#{fixes} in #{current_branch}\n" + end + fixed = nil + end + rewritten_message << line + end + return rewritten_message +end + +$log.debug "Running prepare-forward-merge hook script" + +message_file=ARGV[0] +message_type=ARGV[1] + +if message_type != "merge" + $log.debug "Not a merge commit" + exit 0; +end + +$log.debug "Searching for forward merge" +fixed = get_fixed_issues() +rewritten_message = rewrite_message(message_file, fixed) +File.write(message_file, rewritten_message) diff --git a/gradle.properties b/gradle.properties index 182e5cad1..5cc794136 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,9 @@ -version=1.1.4.BUILD-SNAPSHOT -org.gradle.jvmargs=-XX:MaxPermSize=512M -org.gradle.daemon=false +version=4.0.0-SNAPSHOT + +org.gradle.caching=true +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 +org.gradle.parallel=true + +javaFormatVersion=0.0.43 +jmustacheVersion=1.15 +springFrameworkVersion=7.0.0-M1 diff --git a/gradle/publish-maven.gradle b/gradle/publish-maven.gradle index 94b5e564e..3c50ccb7f 100644 --- a/gradle/publish-maven.gradle +++ b/gradle/publish-maven.gradle @@ -1,45 +1,62 @@ -install { - repositories.mavenInstaller { - customizePom(pom, project) - } -} - -def customizePom(pom, gradleProject) { - pom.whenConfigured { generatedPom -> - generatedPom.dependencies.removeAll { dep -> - dep.scope == "test" - } - generatedPom.project { - name = gradleProject.description - description = gradleProject.description - url = "https://github.com/spring-projects/spring-restdocs" - organization { - name = "Spring IO" - url = "https://projects.spring.io/spring-restdocs" - } - licenses { - license { - name "The Apache Software License, Version 2.0" - url "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution "repo" +plugins.withType(MavenPublishPlugin) { + publishing { + if (project.hasProperty("deploymentRepository")) { + repositories { + maven { + url = project.property("deploymentRepository") + name = "deployment" } } - scm { - url = "https://github.com/spring-projects/spring-restdocs" - connection = "scm:git:git://github.com/spring-projects/spring-restdocs" - developerConnection = "scm:git:git://github.com/spring-projects/spring-restdocs" - } - developers { - developer { - id = "awilkinson" - name = "Andy Wilkinson" - email = "awilkinson@pivotal.io" + } + publications { + maven(MavenPublication) { publication -> + project.plugins.withType(JavaPlugin) { + from components.java + versionMapping { + usage("java-api") { + fromResolutionResult() + } + usage("java-runtime") { + fromResolutionResult() + } + } + } + project.plugins.withType(JavaPlatformPlugin) { + from components.javaPlatform + } + pom { + name = project.provider { project.description } + description = project.provider { project.description } + url = "https://github.com/spring-projects/spring-restdocs" + organization { + name = "Spring IO" + url = "https://projects.spring.io/spring-restdocs" + } + licenses { + license { + name = "The Apache Software License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "repo" + } + } + scm { + url = "https://github.com/spring-projects/spring-restdocs" + connection = "scm:git:git://github.com/spring-projects/spring-restdocs" + developerConnection = "scm:git:git://github.com/spring-projects/spring-restdocs" + } + developers { + developer { + id = "awilkinson" + name = "Andy Wilkinson" + email = "awilkinson@pivotal.io" + } + } + issueManagement { + system = "GitHub" + url = "https://github.com/spring-projects/spring-restdocs/issues" + } } - } - issueManagement { - system = "GitHub" - url = "https://github.com/spring-projects/spring-restdocs/issues" } } } -} \ No newline at end of file +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5ccda13e9..1b33c55ba 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0b95e5d4b..002b867c4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Mon Apr 04 11:55:32 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip diff --git a/gradlew b/gradlew index 9d82f7891..23d15a936 100755 --- a/gradlew +++ b/gradlew @@ -1,74 +1,129 @@ -#!/usr/bin/env bash +#!/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. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# 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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null +CLASSPATH="\\\"\\\"" -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" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -77,84 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# 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, 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" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 5f192121e..db3a6ac20 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -8,26 +26,30 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@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= - set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@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%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -35,54 +57,36 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle deleted file mode 100644 index 709445353..000000000 --- a/samples/rest-assured/build.gradle +++ /dev/null @@ -1,59 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' - } -} - -plugins { - id "org.asciidoctor.convert" version "1.5.3" -} - -apply plugin: 'java' -apply plugin: 'spring-boot' -apply plugin: 'eclipse' - -repositories { - mavenLocal() - maven { url 'https://repo.spring.io/libs-snapshot' } - mavenCentral() -} - -group = 'com.example' - -sourceCompatibility = 1.7 -targetCompatibility = 1.7 - -ext { - snippetsDir = file('build/generated-snippets') -} - -ext['spring-restdocs.version'] = '1.1.4.BUILD-SNAPSHOT' - -dependencies { - compile 'org.springframework.boot:spring-boot-starter-web' - testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile "org.springframework.restdocs:spring-restdocs-restassured:${project.ext['spring-restdocs.version']}" -} - -test { - outputs.dir snippetsDir -} - -asciidoctor { - attributes 'snippets': snippetsDir - inputs.dir snippetsDir - dependsOn test -} - -jar { - dependsOn asciidoctor - from ("${asciidoctor.outputDir}/html5") { - into 'static/docs' - } -} - -eclipseJdt.onlyIf { false } -cleanEclipseJdt.onlyIf { false } diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar b/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 941144813..000000000 Binary files a/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 72a999e4b..000000000 --- a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Feb 15 17:16:16 GMT 2016 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew deleted file mode 100755 index 9d82f7891..000000000 --- a/samples/rest-assured/gradlew +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -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" - which java >/dev/null 2>&1 || 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 - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/rest-assured/gradlew.bat b/samples/rest-assured/gradlew.bat deleted file mode 100644 index 8a0b282aa..000000000 --- a/samples/rest-assured/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@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 - -@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= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -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 init - -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 - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -: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 %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/rest-assured/src/docs/asciidoc/index.adoc b/samples/rest-assured/src/docs/asciidoc/index.adoc deleted file mode 100644 index f89aeb6e1..000000000 --- a/samples/rest-assured/src/docs/asciidoc/index.adoc +++ /dev/null @@ -1,26 +0,0 @@ -= Spring REST Docs REST Assured Sample -Andy Wilkinson; -:doctype: book -:icons: font -:source-highlighter: highlightjs - -Sample application demonstrating how to use Spring REST Docs with REST Assured. - -`SampleRestAssuredApplicationTests` makes a call to a very simple service. The service -that is being tested is running on a random port on `localhost`. The tests make use of a -preprocessor to modify the request so that it appears to have been sent to -`https://api.example.com`. If your service includes URIs in its responses, for example -because it uses hypermedia, similar preprocessing can be applied to the response before -it is documented. - -Three snippets are produced. One showing how to make a request using cURL: - -include::{snippets}/sample/curl-request.adoc[] - -One showing the HTTP request: - -include::{snippets}/sample/http-request.adoc[] - -And one showing the HTTP response: - -include::{snippets}/sample/http-response.adoc[] \ No newline at end of file diff --git a/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java b/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java deleted file mode 100644 index 1c127db72..000000000 --- a/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example.restassured; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@SpringBootApplication -public class SampleRestAssuredApplication { - - public static void main(String[] args) { - new SpringApplication(SampleRestAssuredApplication.class).run(args); - } - - @RestController - private static class SampleController { - - @RequestMapping("/") - public String index() { - return "Hello, World"; - } - - } - -} diff --git a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java deleted file mode 100644 index 0090b7f0c..000000000 --- a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example.restassured; - -import static com.jayway.restassured.RestAssured.given; -import static org.hamcrest.CoreMatchers.is; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.boot.test.WebIntegrationTest; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import com.jayway.restassured.builder.RequestSpecBuilder; -import com.jayway.restassured.specification.RequestSpecification; - -@SpringApplicationConfiguration(classes=SampleRestAssuredApplication.class) -@WebIntegrationTest("server.port=0") -@RunWith(SpringJUnit4ClassRunner.class) -public class SampleRestAssuredApplicationTests { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); - - private RequestSpecification documentationSpec; - - @Value("${local.server.port}") - private int port; - - @Before - public void setUp() { - this.documentationSpec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(restDocumentation)).build(); - } - - @Test - public void sample() throws Exception { - given(this.documentationSpec) - .accept("text/plain") - .filter(document("sample", - preprocessRequest(modifyUris() - .scheme("https") - .host("api.example.com") - .removePort()))) - .when() - .port(this.port) - .get("/") - .then() - .assertThat().statusCode(is(200)); - } - -} diff --git a/samples/rest-notes-grails/.gitignore b/samples/rest-notes-grails/.gitignore deleted file mode 100644 index a521ca89f..000000000 --- a/samples/rest-notes-grails/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -Thumbs.db -.DS_Store -.gradle -build/ -classes/ -.idea -*.iml -*.ipr -*.iws -.project -.settings -.classpath -gradlew* -gradle/wrapper - - -src/docs/generated-snippets diff --git a/samples/rest-notes-grails/README.md b/samples/rest-notes-grails/README.md deleted file mode 100644 index 0e62fb32b..000000000 --- a/samples/rest-notes-grails/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# rest-notes-grails - -## Overview - -This is a sample project using Grails 3, Spock, and Spring REST docs. For more -information about the Grails framework please see [grails.org](https://grails.org). - -Grails is built on top of Spring Boot and Gradle so there are a few different ways to -run this project including: - -### Gradle Wrapper (recommended) - -The gradle wrapper allows a project to build without having Gradle installed locally. The -executable file will acquire the version of Gradle and other dependencies recommended for -this project. This is especially important since some versions of Gradle may cause -conflicts with this project. - -On Unix-like platforms, such as Linux and Mac OS X: - -``` -$ ./gradlew run -``` - -On Windows: - -``` -$ gradlew run -``` - -*Please note*, if you are including integration tests in Grails, they will not run as -part of the `gradle test` task. Run them via the build task or individually as -`gradle integrationTest` - -### Gradle Command Line - -Clean the project: - -``` -$ gradle clean -``` - -Build the project: - -``` -$ gradle build -``` - -Run the project: - -``` -$ gradle run -``` - -### Grails Command Line - -Grails applications also have a command line feature useful for code generation and -running projects locally. The command line is accessible by typing `grails` in the -terminal at the root of the project. Please ensure you are running the correct version -of Grails as specified in [gradle.properties](gradle.properties) - -Similar to `gradle clean`, this task destroys the `build` directory and cached assets. - -``` -grails> clean -``` - -The 'test-app' task runs all of the tests for the project. - -``` -grails> test-app -``` - -The `run-app` task is used to run the application locally. By default, the project is -run in development mode including automatic reloading and not caching static assets. It -is not suggested to use this in production. - -``` -grails> run-app -``` - -### Building and Viewing the Docs - -This is an example of the Grails API profile. Therefore, there is no view layer to -return the docs as static assets. The result of running `asciidoctor` or `build` is that -the docs are sent to `/build/asciidoc/`. You can then publish them to a destination of -your choosing using the [gradle github-pages](https://github.com/ajoberstar/gradle-git) -plugin or similar. - -To just generate documentation and not run the application use: - -``` -$ ./gradlew asciidoctor -``` diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle deleted file mode 100644 index 0d3f76533..000000000 --- a/samples/rest-notes-grails/build.gradle +++ /dev/null @@ -1,107 +0,0 @@ -buildscript { - ext { - grailsVersion = project.grailsVersion - } - repositories { - mavenLocal() - maven { url "https://repo.grails.org/grails/core" } - maven { url 'https://repo.spring.io/libs-snapshot' } - } - dependencies { - classpath "org.grails:grails-gradle-plugin:$grailsVersion" - classpath "org.grails.plugins:hibernate:4.3.10.5" - classpath 'org.ajoberstar:gradle-git:1.1.0' - } -} - -plugins { - id "io.spring.dependency-management" version "0.5.4.RELEASE" - id 'org.asciidoctor.convert' version '1.5.3' -} - -version "0.1" -group "com.example" - -apply plugin: "spring-boot" -apply plugin: "war" -apply plugin: 'eclipse' -apply plugin: 'idea' -apply plugin: "org.grails.grails-web" - -ext { - grailsVersion = project.grailsVersion - gradleWrapperVersion = project.gradleWrapperVersion - restDocsVersion = "1.1.4.BUILD-SNAPSHOT" - snippetsDir = file('src/docs/generated-snippets') -} - -repositories { - mavenLocal() - maven { url 'https://repo.spring.io/libs-snapshot' } - maven { url "https://repo.grails.org/grails/core" } -} - -dependencyManagement { - dependencies { - dependency "org.springframework.restdocs:spring-restdocs-restassured:$restDocsVersion" - } - imports { - mavenBom "org.grails:grails-bom:$grailsVersion" - } - applyMavenExclusions false -} - -dependencies { - compile "org.springframework.boot:spring-boot-starter-logging" - compile "org.springframework.boot:spring-boot-starter-actuator" - compile "org.springframework.boot:spring-boot-autoconfigure" - compile "org.springframework.boot:spring-boot-starter-tomcat" - compile "org.grails:grails-plugin-url-mappings" - compile "org.grails:grails-plugin-rest" - compile "org.grails:grails-plugin-interceptors" - compile "org.grails:grails-plugin-services" - compile "org.grails:grails-plugin-datasource" - compile "org.grails:grails-plugin-databinding" - compile "org.grails:grails-plugin-async" - compile "org.grails:grails-web-boot" - compile "org.grails:grails-logging" - - compile "org.grails.plugins:hibernate" - compile "org.grails.plugins:cache" - compile "org.hibernate:hibernate-ehcache" - - runtime "com.h2database:h2" - - testCompile "org.grails:grails-plugin-testing" - testCompile "org.grails.plugins:geb" - testCompile 'org.springframework.restdocs:spring-restdocs-restassured' - - console "org.grails:grails-console" -} - -task wrapper(type: Wrapper) { - gradleVersion = gradleWrapperVersion -} - -ext { - snippetsDir = file('src/docs/generated-snippets') -} - -task cleanTempDirs(type: Delete) { - delete fileTree(dir: 'src/docs/generated-snippets') -} - -test { - dependsOn cleanTempDirs - outputs.dir snippetsDir -} - -asciidoctor { - dependsOn integrationTest - inputs.dir snippetsDir - sourceDir = file('src/docs') - separateOutputDirs = false - attributes 'snippets': snippetsDir -} - -build.dependsOn asciidoctor diff --git a/samples/rest-notes-grails/gradle.properties b/samples/rest-notes-grails/gradle.properties deleted file mode 100644 index 1b1c50f8e..000000000 --- a/samples/rest-notes-grails/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -grailsVersion=3.0.15 -gradleWrapperVersion=2.3 diff --git a/samples/rest-notes-grails/grails-app/conf/application.yml b/samples/rest-notes-grails/grails-app/conf/application.yml deleted file mode 100644 index 3fe5b2978..000000000 --- a/samples/rest-notes-grails/grails-app/conf/application.yml +++ /dev/null @@ -1,100 +0,0 @@ ---- -grails: - profile: web-api - codegen: - defaultPackage: com.example -info: - app: - name: '@info.app.name@' - version: '@info.app.version@' - grailsVersion: '@info.app.grailsVersion@' -spring: - groovy: - template: - check-template-location: false - ---- -grails: - mime: - disable: - accept: - header: - userAgents: - - Gecko - - WebKit - - Presto - - Trident - types: - all: '*/*' - atom: application/atom+xml - css: text/css - csv: text/csv - form: application/x-www-form-urlencoded - html: - - text/html - - application/xhtml+xml - js: text/javascript - json: - - application/json - - text/json - multipartForm: multipart/form-data - rss: application/rss+xml - text: text/plain - hal: - - application/hal+json - - application/hal+xml - xml: - - text/xml - - application/xml - urlmapping: - cache: - maxsize: 1000 - controllers: - defaultScope: singleton - converters: - encoding: UTF-8 - hibernate: - cache: - queries: false - ---- -dataSource: - pooled: true - jmxExport: true - driverClassName: org.h2.Driver - username: sa - password: - -environments: - development: - dataSource: - dbCreate: create-drop - url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE - test: - dataSource: - dbCreate: update - url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE - server: - port: 0 - production: - dataSource: - dbCreate: update - url: jdbc:h2:./prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE - properties: - jmxEnabled: true - initialSize: 5 - maxActive: 50 - minIdle: 5 - maxIdle: 25 - maxWait: 10000 - maxAge: 600000 - timeBetweenEvictionRunsMillis: 5000 - minEvictableIdleTimeMillis: 60000 - validationQuery: SELECT 1 - validationQueryTimeout: 3 - validationInterval: 15000 - testOnBorrow: true - testWhileIdle: true - testOnReturn: false - jdbcInterceptors: ConnectionState - defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED diff --git a/samples/rest-notes-grails/grails-app/conf/logback.groovy b/samples/rest-notes-grails/grails-app/conf/logback.groovy deleted file mode 100644 index 41467f2a0..000000000 --- a/samples/rest-notes-grails/grails-app/conf/logback.groovy +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -import grails.util.BuildSettings -import grails.util.Environment - -// See https://logback.qos.ch/manual/groovy.html for details on configuration -appender('STDOUT', ConsoleAppender) { - encoder(PatternLayoutEncoder) { - pattern = "%level %logger - %msg%n" - } -} - -root(ERROR, ['STDOUT']) - -def targetDir = BuildSettings.TARGET_DIR -if (Environment.isDevelopmentMode() && targetDir) { - appender("FULL_STACKTRACE", FileAppender) { - file = "${targetDir}/stacktrace.log" - append = true - encoder(PatternLayoutEncoder) { - pattern = "%level %logger - %msg%n" - } - } - logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) -} diff --git a/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy b/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy deleted file mode 100644 index 4907ee437..000000000 --- a/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy +++ /dev/null @@ -1,2 +0,0 @@ -// Place your Spring DSL code here -beans = {} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy deleted file mode 100644 index 012089307..000000000 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example - -import grails.core.GrailsApplication -import grails.util.Environment - -class IndexController { - - GrailsApplication grailsApplication - - def index() { - render(contentType: 'application/json') { - message = "Welcome to Grails!" - environment = Environment.current.name - appversion = grailsApplication.metadata['info.app.version'] - grailsversion = grailsApplication.metadata['info.app.grailsVersion'] - appprofile = grailsApplication.config.grails?.profile - groovyversion = GroovySystem.getVersion() - jvmversion = System.getProperty('java.version') - controllers = array { - for (c in grailsApplication.controllerClasses) { - controller([name: c.fullName]) - } - } - plugins = array { - for (p in grailsApplication.mainContext.pluginManager.allPlugins) { - plugin([name: p.fullName]) - } - } - } - } - -} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy deleted file mode 100644 index 64a9e7f46..000000000 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example - -class InternalServerErrorController { - - def index() { - render(contentType: 'application/json') { - error = 500 - message = "Internal server error" - } - } - -} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy deleted file mode 100644 index 14e2afbd4..000000000 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example - -class NotFoundController { - - def index() { - render(contentType: 'application/json') { - error = 404 - message = "Not Found" - } - } - -} diff --git a/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy b/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy deleted file mode 100644 index e4a3346a1..000000000 --- a/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example - -import grails.rest.Resource - -@Resource(uri='/notes', formats = ['json', 'xml']) -class Note { - - Long id - - String title - - String body - - static hasMany = [tags: Tag] - - static mapping = { - tags joinTable: [name: "mm_notes_tags", key: 'mm_note_id' ] - } - -} - diff --git a/samples/rest-notes-grails/grails-app/i18n/messages.properties b/samples/rest-notes-grails/grails-app/i18n/messages.properties deleted file mode 100644 index b04513621..000000000 --- a/samples/rest-notes-grails/grails-app/i18n/messages.properties +++ /dev/null @@ -1,56 +0,0 @@ -default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] -default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL -default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number -default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address -default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}] -default.invalid.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size range from [{3}] to [{4}] -default.invalid.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}] -default.invalid.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}] -default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}] -default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}] -default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation -default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}] -default.blank.message=Property [{0}] of class [{1}] cannot be blank -default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}] -default.null.message=Property [{0}] of class [{1}] cannot be null -default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique - -default.paginate.prev=Previous -default.paginate.next=Next -default.boolean.true=True -default.boolean.false=False -default.date.format=yyyy-MM-dd HH:mm:ss z -default.number.format=0 - -default.created.message={0} {1} created -default.updated.message={0} {1} updated -default.deleted.message={0} {1} deleted -default.not.deleted.message={0} {1} could not be deleted -default.not.found.message={0} not found with id {1} -default.optimistic.locking.failure=Another user has updated this {0} while you were editing - -default.home.label=Home -default.list.label={0} List -default.add.label=Add {0} -default.new.label=New {0} -default.create.label=Create {0} -default.show.label=Show {0} -default.edit.label=Edit {0} - -default.button.create.label=Create -default.button.edit.label=Edit -default.button.update.label=Update -default.button.delete.label=Delete -default.button.delete.confirm.message=Are you sure? - -# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) -typeMismatch.java.net.URL=Property {0} must be a valid URL -typeMismatch.java.net.URI=Property {0} must be a valid URI -typeMismatch.java.util.Date=Property {0} must be a valid Date -typeMismatch.java.lang.Double=Property {0} must be a valid number -typeMismatch.java.lang.Integer=Property {0} must be a valid number -typeMismatch.java.lang.Long=Property {0} must be a valid number -typeMismatch.java.lang.Short=Property {0} must be a valid number -typeMismatch.java.math.BigDecimal=Property {0} must be a valid number -typeMismatch.java.math.BigInteger=Property {0} must be a valid number -typeMismatch=Property {0} is type-mismatched diff --git a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy b/samples/rest-notes-grails/grails-app/init/BootStrap.groovy deleted file mode 100644 index c3255d1e1..000000000 --- a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -import com.example.Note - -class BootStrap { - - def init = { servletContext -> - environments { - test { - new Note(title: 'Hello, World!', body: 'Hello from the Integration Test').save() - } - } - } - - def destroy = {} - -} diff --git a/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy b/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy deleted file mode 100644 index 86736fa40..000000000 --- a/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example - -import grails.boot.GrailsApp -import grails.boot.config.GrailsAutoConfiguration - -class Application extends GrailsAutoConfiguration { - - static void main(String[] args) { - GrailsApp.run(Application, args) - } - -} diff --git a/samples/rest-notes-grails/src/docs/index.adoc b/samples/rest-notes-grails/src/docs/index.adoc deleted file mode 100644 index 231af2ecf..000000000 --- a/samples/rest-notes-grails/src/docs/index.adoc +++ /dev/null @@ -1,148 +0,0 @@ -= Grails RESTful Notes API Guide -Andy Wilkinson; Jenn Strater -:doctype: book -:icons: font -:source-highlighter: highlightjs -:toc: left -:toclevels: 4 -:sectlinks: - -[[overview]] -= Overview - -[[overview-http-verbs]] -== HTTP verbs - -Grails RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP verbs. - -|=== -| Verb | Usage - -| `GET` -| Used to retrieve a resource - -| `POST` -| Used to create a new resource - -| `PATCH` -| Used to update an existing resource, including partial updates - -| `DELETE` -| Used to delete an existing resource -|=== - -[[overview-http-status-codes]] -== HTTP status codes - -Grails RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP status codes. - -|=== -| Status code | Usage - -| `200 OK` -| The request completed successfully - -| `201 Created` -| A new resource has been created successfully. The resource's URI is available from the response's -`Location` header - -| `204 No Content` -| An update to an existing resource has been applied successfully - -| `400 Bad Request` -| The request was malformed. The response body will include an error providing further information - -| `404 Not Found` -| The requested resource did not exist -|=== - -[[resources]] -= Resources - - -[[resources-index]] -== Index - -The index provides the entry point into the service. - - - -[[resources-index-access]] -=== Accessing the index - -A `GET` request is used to access the index - -==== Example request - -include::{snippets}/index-example/curl-request.adoc[] - -==== Response structure - -include::{snippets}/index-example/response-fields.adoc[] - -==== Example response - -include::{snippets}/index-example/http-response.adoc[] - - -[[resources-notes]] -== Notes - -The Notes resources is used to create and list notes - - - -[[resources-notes-list]] -=== Listing notes - -A `GET` request will list all of the service's notes. - -==== Response structure - -include::{snippets}/notes-list-example/response-fields.adoc[] - -==== Example request - -include::{snippets}/notes-list-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/notes-list-example/http-response.adoc[] - - - -[[resources-notes-create]] -=== Creating a note - -A `POST` request is used to create a note - -==== Request structure - -include::{snippets}/notes-create-example/request-fields.adoc[] - -==== Example request - -include::{snippets}/notes-create-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/notes-create-example/http-response.adoc[] - -[[resources-note-retrieve]] -=== Retrieve a note - -A `GET` request will retrieve the details of a note - -==== Response structure - -include::{snippets}/note-get-example/response-fields.adoc[] - -==== Example request - -include::{snippets}/note-get-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/note-get-example/http-response.adoc[] diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy deleted file mode 100644 index 75e4b003f..000000000 --- a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example - -import org.springframework.restdocs.payload.JsonFieldType - -import static com.jayway.restassured.RestAssured.given -import static org.hamcrest.CoreMatchers.is -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse -import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields -import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration - -import com.jayway.restassured.builder.RequestSpecBuilder -import com.jayway.restassured.specification.RequestSpecification -import grails.test.mixin.integration.Integration -import grails.transaction.Rollback -import org.junit.Rule -import org.springframework.beans.factory.annotation.Value -import org.springframework.http.MediaType -import org.springframework.restdocs.JUnitRestDocumentation -import spock.lang.Specification - -@Integration -@Rollback -class ApiDocumentationSpec extends Specification { - - @Rule - JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/docs/generated-snippets') - - @Value('${local.server.port}') - Integer serverPort - - protected RequestSpecification documentationSpec - - void setup() { - this.documentationSpec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(restDocumentation)) - .build() - } - - void 'test and document get request for /index'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .filter(document('index-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath('message').description('Welcome to Grails!'), - fieldWithPath('environment').description("The running environment"), - fieldWithPath('appversion').description('version of the app that is running'), - fieldWithPath('grailsversion').description('the version of grails used in this project'), - fieldWithPath('appprofile').description('the profile of grails used in this project'), - fieldWithPath('groovyversion').description('the version of groovy used in this project'), - fieldWithPath('jvmversion').description('the version of the jvm used in this project'), - fieldWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), - fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), - ))) - .when() - .port(this.serverPort) - .get('/') - .then() - .assertThat() - .statusCode(is(200)) - } - - void 'test and document notes list request'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .filter(document('notes-list-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath('[].class').description('the class of the resource'), - fieldWithPath('[].id').description('the id of the note'), - fieldWithPath('[].title').description('the title of the note'), - fieldWithPath('[].body').description('the body of the note'), - fieldWithPath('[].tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), - ))) - .when() - .port(this.serverPort) - .get('/notes') - .then() - .assertThat() - .statusCode(is(200)) - } - - void 'test and document create new note'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .contentType(MediaType.APPLICATION_JSON.toString()) - .filter(document('notes-create-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - requestFields( - fieldWithPath('title').description('the title of the note'), - fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') - ), - responseFields( - fieldWithPath('class').description('the class of the resource'), - fieldWithPath('id').description('the id of the note'), - fieldWithPath('title').description('the title of the note'), - fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), - ))) - .body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }') - .when() - .port(this.serverPort) - .post('/notes') - .then() - .assertThat() - .statusCode(is(201)) - } - - void 'test and document getting specific note'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .filter(document('note-get-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath('class').description('the class of the resource'), - fieldWithPath('id').description('the id of the note'), - fieldWithPath('title').description('the title of the note'), - fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), - ))) - .when() - .port(this.serverPort) - .get('/notes/1') - .then() - .assertThat() - .statusCode(is(200)) - } - -} diff --git a/samples/rest-notes-slate/README.md b/samples/rest-notes-slate/README.md deleted file mode 100644 index 37f6935ed..000000000 --- a/samples/rest-notes-slate/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# REST Notes Slate - -This sample shows how to document a RESTful API using Spring REST Docs and [Slate][1]. -The sample can be built with Gradle, but requires Ruby and the `bundler` gem to -be installed. - -## Quickstart - -``` -./gradlew build -open build/docs/api-guide.html -``` - -## Details - -The bulk of the documentation is written in Markdown in the file named -[slate/api-guide.md.erb][2]. When the documentation is built, snippets generated by -Spring REST Docs in the [ApiDocumentation][3] tests are incorporated into this -documentation by [ERB][4]. The combined Markdown document is then turned into HTML. - -[1]: https://github.com/tripit/slate -[2]: slate/api-guide.md.erb -[3]: src/test/java/com/example/notes/ApiDocumentation.java -[4]: https://ruby-doc.org/stdlib-2.2.3/libdoc/erb/rdoc/ERB.html \ No newline at end of file diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle deleted file mode 100644 index c13564f95..000000000 --- a/samples/rest-notes-slate/build.gradle +++ /dev/null @@ -1,65 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' - } -} - -apply plugin: 'java' -apply plugin: 'spring-boot' -apply plugin: 'eclipse' - -repositories { - mavenLocal() - maven { url 'https://repo.spring.io/libs-snapshot' } - mavenCentral() -} - -group = 'com.example' - -sourceCompatibility = 1.7 -targetCompatibility = 1.7 - -ext { - snippetsDir = file('build/generated-snippets') -} - -ext['spring-restdocs.version'] = '1.1.4.BUILD-SNAPSHOT' - -dependencies { - compile 'org.springframework.boot:spring-boot-starter-data-jpa' - compile 'org.springframework.boot:spring-boot-starter-data-rest' - - runtime 'com.h2database:h2' - runtime 'org.atteo:evo-inflector:1.2.1' - - testCompile 'com.jayway.jsonpath:json-path' - testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' -} - -test { - outputs.dir snippetsDir -} - -task(bundleInstall, type: Exec) { - workingDir file('slate') - executable 'bundle' - args 'install' -} - -task(slate, type: Exec) { - dependsOn 'bundleInstall', 'test' - workingDir file('slate') - executable 'bundle' - args 'exec', 'middleman', 'build' -} - -build { - dependsOn 'slate' -} - -eclipseJdt.onlyIf { false } -cleanEclipseJdt.onlyIf { false } diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e8c6bf7bb..000000000 Binary files a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 90a0f04cd..000000000 --- a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Thu Oct 15 14:25:16 BST 2015 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-bin.zip diff --git a/samples/rest-notes-slate/gradlew b/samples/rest-notes-slate/gradlew deleted file mode 100755 index 97fac783e..000000000 --- a/samples/rest-notes-slate/gradlew +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - -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" - which java >/dev/null 2>&1 || 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 - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/rest-notes-slate/gradlew.bat b/samples/rest-notes-slate/gradlew.bat deleted file mode 100644 index 8a0b282aa..000000000 --- a/samples/rest-notes-slate/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@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 - -@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= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -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 init - -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 - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -: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 %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/rest-notes-slate/slate/.gitignore b/samples/rest-notes-slate/slate/.gitignore deleted file mode 100644 index f6fc8c00b..000000000 --- a/samples/rest-notes-slate/slate/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -*.gem -*.rbc -.bundle -.config -coverage -InstalledFiles -lib/bundler/man -pkg -rdoc -spec/reports -test/tmp -test/version_tmp -tmp -*.DS_STORE -build/ -.cache - -# YARD artifacts -.yardoc -_yardoc -doc/ -.idea/ \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/CHANGELOG.md b/samples/rest-notes-slate/slate/CHANGELOG.md deleted file mode 100644 index ee496db6c..000000000 --- a/samples/rest-notes-slate/slate/CHANGELOG.md +++ /dev/null @@ -1,49 +0,0 @@ -# Changelog - -## Version 1.2 - -*June 20, 2015* - -**Fixes:** - -- Remove crash on invalid languages -- Update Tocify to scroll to the highlighted header in the Table of Contents -- Fix variable leak and update search algorithms -- Update Python examples to be valid Python -- Update gems -- More misc. bugfixes of Javascript errors -- Add Dockerfile -- Remove unused gems -- Optimize images, fonts, and generated asset files -- Add chinese font support -- Remove RedCarpet header ID patch -- Update language tabs to not disturb existing query strings - -## Version 1.1 - -*July 27th, 2014* - -**Fixes:** - -- Finally, a fix for the redcarpet upgrade bug - -## Version 1.0 - -*July 2, 2014* - -[View Issues](https://github.com/tripit/slate/issues?milestone=1&state=closed) - -**Features:** - -- Responsive designs for phones and tablets -- Started tagging versions - -**Fixes:** - -- Fixed 'unrecognized expression' error -- Fixed #undefined hash bug -- Fixed bug where the current language tab would be unselected -- Fixed bug where tocify wouldn't highlight the current section while searching -- Fixed bug where ids of header tags would have special characters that caused problems -- Updated layout so that pages with disabled search wouldn't load search.js -- Cleaned up Javascript diff --git a/samples/rest-notes-slate/slate/Gemfile b/samples/rest-notes-slate/slate/Gemfile deleted file mode 100644 index 0933b9d68..000000000 --- a/samples/rest-notes-slate/slate/Gemfile +++ /dev/null @@ -1,12 +0,0 @@ -source 'https://rubygems.org' - -# Middleman -gem 'middleman', '~>3.3.10' -gem 'middleman-gh-pages', '~> 0.0.3' -gem 'middleman-syntax', '~> 2.0.0' -gem 'middleman-autoprefixer', '~> 2.4.4' -gem 'rouge', '~> 1.9.0' -gem 'redcarpet', '~> 3.3.2' - -gem 'rake', '~> 10.4.2' -gem 'therubyracer', '~> 0.12.1', platforms: :ruby diff --git a/samples/rest-notes-slate/slate/Gemfile.lock b/samples/rest-notes-slate/slate/Gemfile.lock deleted file mode 100644 index fff5ee10c..000000000 --- a/samples/rest-notes-slate/slate/Gemfile.lock +++ /dev/null @@ -1,140 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - activesupport (4.1.11) - i18n (~> 0.6, >= 0.6.9) - json (~> 1.7, >= 1.7.7) - minitest (~> 5.1) - thread_safe (~> 0.1) - tzinfo (~> 1.1) - autoprefixer-rails (5.2.0.1) - execjs - json - celluloid (0.16.0) - timers (~> 4.0.0) - chunky_png (1.3.4) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.9.1.1) - compass (1.0.3) - chunky_png (~> 1.2) - compass-core (~> 1.0.2) - compass-import-once (~> 1.0.5) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) - sass (>= 3.3.13, < 3.5) - compass-core (1.0.3) - multi_json (~> 1.0) - sass (>= 3.3.0, < 3.5) - compass-import-once (1.0.5) - sass (>= 3.2, < 3.5) - erubis (2.7.0) - execjs (2.5.2) - ffi (1.9.8) - haml (4.0.6) - tilt - hike (1.2.3) - hitimes (1.2.2) - hooks (0.4.0) - uber (~> 0.0.4) - i18n (0.7.0) - json (1.8.3) - kramdown (1.7.0) - libv8 (3.16.14.7) - listen (2.10.1) - celluloid (~> 0.16.0) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) - middleman (3.3.12) - coffee-script (~> 2.2) - compass (>= 1.0.0, < 2.0.0) - compass-import-once (= 1.0.5) - execjs (~> 2.0) - haml (>= 4.0.5) - kramdown (~> 1.2) - middleman-core (= 3.3.12) - middleman-sprockets (>= 3.1.2) - sass (>= 3.4.0, < 4.0) - uglifier (~> 2.5) - middleman-autoprefixer (2.4.4) - autoprefixer-rails (~> 5.2.0) - middleman-core (>= 3.3.3) - middleman-core (3.3.12) - activesupport (~> 4.1.0) - bundler (~> 1.1) - erubis - hooks (~> 0.3) - i18n (~> 0.7.0) - listen (>= 2.7.9, < 3.0) - padrino-helpers (~> 0.12.3) - rack (>= 1.4.5, < 2.0) - rack-test (~> 0.6.2) - thor (>= 0.15.2, < 2.0) - tilt (~> 1.4.1, < 2.0) - middleman-gh-pages (0.0.3) - rake (> 0.9.3) - middleman-sprockets (3.4.2) - middleman-core (>= 3.3) - sprockets (~> 2.12.1) - sprockets-helpers (~> 1.1.0) - sprockets-sass (~> 1.3.0) - middleman-syntax (2.0.0) - middleman-core (~> 3.2) - rouge (~> 1.0) - minitest (5.7.0) - multi_json (1.11.1) - padrino-helpers (0.12.5) - i18n (~> 0.6, >= 0.6.7) - padrino-support (= 0.12.5) - tilt (~> 1.4.1) - padrino-support (0.12.5) - activesupport (>= 3.1) - rack (1.6.4) - rack-test (0.6.3) - rack (>= 1.0) - rake (10.4.2) - rb-fsevent (0.9.5) - rb-inotify (0.9.5) - ffi (>= 0.5.0) - redcarpet (3.3.2) - ref (1.0.5) - rouge (1.9.0) - sass (3.4.14) - sprockets (2.12.3) - hike (~> 1.2) - multi_json (~> 1.0) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - sprockets-helpers (1.1.0) - sprockets (~> 2.0) - sprockets-sass (1.3.1) - sprockets (~> 2.0) - tilt (~> 1.1) - therubyracer (0.12.2) - libv8 (~> 3.16.14.0) - ref - thor (0.19.1) - thread_safe (0.3.5) - tilt (1.4.1) - timers (4.0.1) - hitimes - tzinfo (1.2.2) - thread_safe (~> 0.1) - uber (0.0.13) - uglifier (2.7.1) - execjs (>= 0.3.0) - json (>= 1.8.0) - -PLATFORMS - ruby - -DEPENDENCIES - middleman (~> 3.3.10) - middleman-autoprefixer (~> 2.4.4) - middleman-gh-pages (~> 0.0.3) - middleman-syntax (~> 2.0.0) - rake (~> 10.4.2) - redcarpet (~> 3.3.2) - rouge (~> 1.9.0) - therubyracer (~> 0.12.1) diff --git a/samples/rest-notes-slate/slate/LICENSE b/samples/rest-notes-slate/slate/LICENSE deleted file mode 100644 index 8edb3189a..000000000 --- a/samples/rest-notes-slate/slate/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2008-2013 Concur Technologies, Inc. - -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. \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/README.md b/samples/rest-notes-slate/slate/README.md deleted file mode 100644 index 68c041283..000000000 --- a/samples/rest-notes-slate/slate/README.md +++ /dev/null @@ -1,128 +0,0 @@ -Slate -======== - -[![Build Status](https://travis-ci.org/tripit/slate.svg?branch=master)](https://travis-ci.org/tripit/slate) [![Dependency Status](https://gemnasium.com/tripit/slate.png)](https://gemnasium.com/tripit/slate) - -Slate helps you create beautiful API documentation. Think of it as an intelligent, responsive documentation template for your API. - -Screenshot of Example Documentation created with Slate - -*The example above was created with Slate. Check it out at [tripit.github.io/slate](https://tripit.github.io/slate).* - -Features ------------- - -* **Clean, intuitive design** — with Slate, the description of your API is on the left side of your documentation, and all the code examples are on the right side. Inspired by [Stripe's](https://stripe.com/docs/api) and [Paypal's](https://developer.paypal.com/webapps/developer/docs/api/) API docs. Slate is responsive, so it looks great on tablets, phones, and even print. - -* **Everything on a single page** — gone are the days where your users had to search through a million pages to find what they wanted. Slate puts the entire documentation on a single page. We haven't sacrificed linkability, though. As you scroll, your browser's hash will update to the nearest header, so linking to a particular point in the documentation is still natural and easy. - -* **Slate is just Markdown** — when you write docs with Slate, you're just writing Markdown, which makes it simple to edit and understand. Everything is written in Markdown — even the code samples are just Markdown code blocks! - -* **Write code samples in multiple languages** — if your API has bindings in multiple programming languages, you easily put in tabs to switch between them. In your document, you'll distinguish different languages by specifying the language name at the top of each code block, just like with Github Flavored Markdown! - -* **Out-of-the-box syntax highlighting** for [almost 60 languages](https://rouge.jayferd.us/demo), no configuration required. - -* **Automatic, smoothly scrolling table of contents** on the far left of the page. As you scroll, it displays your current position in the document. It's fast, too. We're using Slate at TripIt to build documentation for our new API, where our table of contents has over 180 entries. We've made sure that the performance remains excellent, even for larger documents. - -* **Let your users update your documentation for you** — by default, your Slate-generated documentation is hosted in a public Github repository. Not only does this mean you get free hosting for your docs with Github Pages, but it also makes it's simple for other developers to make pull requests to your docs if they find typos or other problems. Of course, if you don't want to, you're welcome to not use Github and host your docs elsewhere! - -Getting starting with Slate is super easy! Simply fork this repository, and then follow the instructions below. Or, if you'd like to check out what Slate is capable of, take a look at the [sample docs](https://tripit.github.io/slate). - - - -Getting Started with Slate ------------------------------- - -### Prerequisites - -You're going to need: - - - **Linux or OS X** — Windows may work, but is unsupported. - - **Ruby, version 1.9.3 or newer** - - **Bundler** — If Ruby is already installed, but the `bundle` command doesn't work, just run `gem install bundler` in a terminal. - -### Getting Set Up - - 1. Fork this repository on Github. - 2. Clone *your forked repository* (not our original one) to your hard drive with `git clone https://github.com/YOURUSERNAME/slate.git` - 3. `cd slate` - 4. Install all dependencies: `bundle install` - 5. Start the test server: `bundle exec middleman server` - -Or use the included Dockerfile! (must install Docker first) - -```shell -docker build -t slate . -docker run -d -p 4567:4567 --name slate -v $(pwd)/source:/app/source slate -``` - -You can now see the docs at . Whoa! That was fast! - -*Note: if you're using the Docker setup on OSX, the docs will be -availalable at the output of `boot2docker ip` instead of `localhost:4567`.* - -Now that Slate is all set up your machine, you'll probably want to learn more about [editing Slate markdown](https://github.com/tripit/slate/wiki/Markdown-Syntax), or [how to publish your docs](https://github.com/tripit/slate/wiki/Deploying-Slate). - -Examples of Slate in the Wild ---------------------------------- - -* [Travis-CI's API docs](https://docs.travis-ci.com/api/) -* [Mozilla's localForage docs](https://mozilla.github.io/localForage/) -* [Mozilla Recroom](https://mozilla.github.io/recroom/) -* [ChaiOne Gameplan API docs](https://chaione.github.io/gameplanb2b/#introduction) -* [Drcaban's Build a Quine tutorial](https://drcabana.github.io/build-a-quine/#introduction) -* [PricePlow API docs](https://www.priceplow.com/api/documentation) -* [Emerging Threats API docs](https://apidocs.emergingthreats.net/) -* [Appium docs](https://appium.io/slate/en/master) -* [Golazon Developer](https://developer.golazon.com) -* [Dwolla API docs](https://docs.dwolla.com/) -* [RozpisyZapasu API docs](https://www.rozpisyzapasu.cz/dev/api/) -* [Codestar Framework Docs](https://codestarframework.com/documentation/) -* [Buddycloud API](http://buddycloud.com/api) -* [Crafty Clicks API](https://craftyclicks.co.uk/api/) -* [Paracel API Reference](http://paracel.io/docs/api_reference.html) -* [Switch Payments Documentation](https://switchpayments.com/docs/) & [API](https://switchpayments.com/developers/) -* [Coinbase API Reference](https://developers.coinbase.com/api) -* [Whispir.io API](https://whispir.github.io/api) -* [NASA API](https://data.nasa.gov/developer/external/planetary/) -* [CardPay API](https://developers.cardpay.com/) -* [IBM Cloudant](https://docs-testb.cloudant.com/content-review/_design/couchapp/index.html) -* [Bitrix basis components](https://bbc.bitrix.expert/) -* [viagogo API Documentation](https://developer.viagogo.net/) -* [Fidor Bank API Documentation](https://docs.fidor.de/) -* [Market Prophit API Documentation](https://developer.marketprophit.com/) -* [OAuth.io API Documentation](https://docs.oauth.io/) -* [Aircall for Developers](https://developer.aircall.io/) -* [SupportKit API Docs](https://docs.smooch.io/) -* [SocialRadar's LocationKit Docs](https://docs.locationkit.io/) -* [SafetyCulture API Documentation](https://developer.safetyculture.io/) -* [hosting.de API Documentation](https://www.hosting.de/docs/api/) - -(Feel free to add your site to this list in a pull request!) - -Need Help? Found a bug? --------------------- - -Just [submit a issue](https://github.com/tripit/slate/issues) to the Slate Github if you need any help. And, of course, feel free to submit pull requests with bug fixes or changes. - - -Contributors --------------------- - -Slate was built by [Robert Lord](https://lord.io) while at [TripIt](https://tripit.com). - -Thanks to the following people who have submitted major pull requests: - -- [@chrissrogers](https://github.com/chrissrogers) -- [@bootstraponline](https://github.com/bootstraponline) -- [@realityking](https://github.com/realityking) - -Also, thanks to [Sauce Labs](https://saucelabs.com) for helping sponsor the project. - -Special Thanks --------------------- -- [Middleman](https://github.com/middleman/middleman) -- [jquery.tocify.js](https://github.com/gfranko/jquery.tocify.js) -- [middleman-syntax](https://github.com/middleman/middleman-syntax) -- [middleman-gh-pages](https://github.com/neo/middleman-gh-pages) -- [Font Awesome](https://fortawesome.github.io/Font-Awesome/) diff --git a/samples/rest-notes-slate/slate/Rakefile b/samples/rest-notes-slate/slate/Rakefile deleted file mode 100644 index 6a952e1e9..000000000 --- a/samples/rest-notes-slate/slate/Rakefile +++ /dev/null @@ -1,6 +0,0 @@ -require 'middleman-gh-pages' -require 'rake/clean' - -CLOBBER.include('build') - -task :default => [:build] diff --git a/samples/rest-notes-slate/slate/config.rb b/samples/rest-notes-slate/slate/config.rb deleted file mode 100644 index fdcb21f32..000000000 --- a/samples/rest-notes-slate/slate/config.rb +++ /dev/null @@ -1,41 +0,0 @@ -# Markdown -set :markdown_engine, :redcarpet -set :markdown, - fenced_code_blocks: true, - smartypants: true, - disable_indented_code_blocks: true, - prettify: true, - tables: true, - with_toc_data: true, - no_intra_emphasis: true - -# Assets -set :css_dir, 'stylesheets' -set :js_dir, 'javascripts' -set :images_dir, 'images' -set :fonts_dir, 'fonts' - -# Activate the syntax highlighter -activate :syntax - -activate :autoprefixer do |config| - config.browsers = ['last 2 version', 'Firefox ESR'] - config.cascade = false - config.inline = true -end - -# Github pages require relative links -activate :relative_assets -set :relative_links, true - -# Build Configuration - -set :build_dir, '../build/docs' - -configure :build do - activate :minify_css - activate :minify_javascript - # activate :relative_assets - # activate :asset_hash - # activate :gzip -end diff --git a/samples/rest-notes-slate/slate/font-selection.json b/samples/rest-notes-slate/slate/font-selection.json deleted file mode 100755 index 5e78f5d86..000000000 --- a/samples/rest-notes-slate/slate/font-selection.json +++ /dev/null @@ -1,148 +0,0 @@ -{ - "IcoMoonType": "selection", - "icons": [ - { - "icon": { - "paths": [ - "M438.857 73.143q119.429 0 220.286 58.857t159.714 159.714 58.857 220.286-58.857 220.286-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857zM512 785.714v-108.571q0-8-5.143-13.429t-12.571-5.429h-109.714q-7.429 0-13.143 5.714t-5.714 13.143v108.571q0 7.429 5.714 13.143t13.143 5.714h109.714q7.429 0 12.571-5.429t5.143-13.429zM510.857 589.143l10.286-354.857q0-6.857-5.714-10.286-5.714-4.571-13.714-4.571h-125.714q-8 0-13.714 4.571-5.714 3.429-5.714 10.286l9.714 354.857q0 5.714 5.714 10t13.714 4.286h105.714q8 0 13.429-4.286t6-10z" - ], - "attrs": [], - "isMulticolor": false, - "tags": [ - "exclamation-circle" - ], - "defaultCode": 61546, - "grid": 14 - }, - "attrs": [], - "properties": { - "id": 100, - "order": 4, - "prevSize": 28, - "code": 58880, - "name": "exclamation-sign", - "ligatures": "" - }, - "setIdx": 0, - "iconIdx": 0 - }, - { - "icon": { - "paths": [ - "M585.143 786.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-54.857v-292.571q0-8-5.143-13.143t-13.143-5.143h-182.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h54.857v182.857h-54.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h256q8 0 13.143-5.143t5.143-13.143zM512 274.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-109.714q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h109.714q8 0 13.143-5.143t5.143-13.143zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z" - ], - "attrs": [], - "isMulticolor": false, - "tags": [ - "info-circle" - ], - "defaultCode": 61530, - "grid": 14 - }, - "attrs": [], - "properties": { - "id": 85, - "order": 3, - "name": "info-sign", - "prevSize": 28, - "code": 58882 - }, - "setIdx": 0, - "iconIdx": 2 - }, - { - "icon": { - "paths": [ - "M733.714 419.429q0-16-10.286-26.286l-52-51.429q-10.857-10.857-25.714-10.857t-25.714 10.857l-233.143 232.571-129.143-129.143q-10.857-10.857-25.714-10.857t-25.714 10.857l-52 51.429q-10.286 10.286-10.286 26.286 0 15.429 10.286 25.714l206.857 206.857q10.857 10.857 25.714 10.857 15.429 0 26.286-10.857l310.286-310.286q10.286-10.286 10.286-25.714zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z" - ], - "attrs": [], - "isMulticolor": false, - "tags": [ - "check-circle" - ], - "defaultCode": 61528, - "grid": 14 - }, - "attrs": [], - "properties": { - "id": 83, - "order": 9, - "prevSize": 28, - "code": 58886, - "name": "ok-sign" - }, - "setIdx": 0, - "iconIdx": 6 - }, - { - "icon": { - "paths": [ - "M658.286 475.429q0-105.714-75.143-180.857t-180.857-75.143-180.857 75.143-75.143 180.857 75.143 180.857 180.857 75.143 180.857-75.143 75.143-180.857zM950.857 950.857q0 29.714-21.714 51.429t-51.429 21.714q-30.857 0-51.429-21.714l-196-195.429q-102.286 70.857-228 70.857-81.714 0-156.286-31.714t-128.571-85.714-85.714-128.571-31.714-156.286 31.714-156.286 85.714-128.571 128.571-85.714 156.286-31.714 156.286 31.714 128.571 85.714 85.714 128.571 31.714 156.286q0 125.714-70.857 228l196 196q21.143 21.143 21.143 51.429z" - ], - "width": 951, - "attrs": [], - "isMulticolor": false, - "tags": [ - "search" - ], - "defaultCode": 61442, - "grid": 14 - }, - "attrs": [], - "properties": { - "id": 2, - "order": 1, - "prevSize": 28, - "code": 58887, - "name": "icon-search" - }, - "setIdx": 0, - "iconIdx": 7 - } - ], - "height": 1024, - "metadata": { - "name": "slate", - "license": "SIL OFL 1.1" - }, - "preferences": { - "showGlyphs": true, - "showQuickUse": true, - "showQuickUse2": true, - "showSVGs": true, - "fontPref": { - "prefix": "icon-", - "metadata": { - "fontFamily": "slate", - "majorVersion": 1, - "minorVersion": 0, - "description": "Based on FontAwesome", - "license": "SIL OFL 1.1" - }, - "metrics": { - "emSize": 1024, - "baseline": 6.25, - "whitespace": 50 - }, - "resetPoint": 58880, - "showSelector": false, - "selector": "class", - "classSelector": ".icon", - "showMetrics": false, - "showMetadata": true, - "showVersion": true, - "ie7": false - }, - "imagePref": { - "prefix": "icon-", - "png": true, - "useClassSelector": true, - "color": 4473924, - "bgColor": 16777215 - }, - "historySize": 100, - "showCodes": true, - "gridSize": 16, - "showLiga": false - } -} diff --git a/samples/rest-notes-slate/slate/source/api-guide.md.erb b/samples/rest-notes-slate/slate/source/api-guide.md.erb deleted file mode 100644 index 77dab5229..000000000 --- a/samples/rest-notes-slate/slate/source/api-guide.md.erb +++ /dev/null @@ -1,206 +0,0 @@ ---- -title: REST Notes API Guide - -language_tabs: - - shell - - http - -search: true ---- - -# Overview - - -## HTTP verbs - -RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP verbs. - - -Verb | Usage --------- | ----- -`GET` | Used to retrieve a resource -`POST` | Used to create a new resource -`PATCH` | Used to update an existing resource, including partial updates -`DELETE` | Used to delete an existing resource - -## HTTP status codes - -RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP status codes. - -Status code | Usage ------------------ | ----- -`200 OK` | The request completed successfully -`201 Created` | A new resource has been created successfully. The resource's URI is available from the response's `Location` header -`204 No Content` | An update to an existing resource has been applied successfully -`400 Bad Request` | The request was malformed. The response body will include an error providing further information -`404 Not Found` | The requested resource did not exist - -## Errors - -<%= ERB.new(File.read("../build/generated-snippets/error-example/http-response.md")).result(binding) %> - -Whenever an error response (status code >= 400) is returned, the body will contain a JSON object -that describes the problem. The error object has the following structure: - -<%= ERB.new(File.read("../build/generated-snippets/error-example/response-fields.md")).result(binding) %> - -## Hypermedia - -RESTful Notes uses hypermedia and resources include links to other resources in their -responses. Responses are in [Hypertext Application Language (HAL)](https://github.com/mikekelly/hal_specification) -format. Links can be found beneath the `_links` key. Users of the API should not create -URIs themselves, instead they should use the above-described links to navigate - - - -# Resources - - - -## Index - -<%= ERB.new(File.read("../build/generated-snippets/index-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/index-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/index-example/http-response.md")).result(binding) %> - -The index provides the entry point into the service. A `GET` request is used to access the index. - -### Response structure - -<%= ERB.new(File.read("../build/generated-snippets/index-example/response-fields.md")).result(binding) %> - -### Links - -<%= ERB.new(File.read("../build/generated-snippets/index-example/links.md")).result(binding) %> - - - -## Notes - -The Notes resources is used to create and list notes - -### Listing notes - -<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/http-response.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/curl-request.md")).result(binding) %> - -A `GET` request will list all of the service's notes. - -#### Response structure - -<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/response-fields.md")).result(binding) %> - -### Creating a note - -<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/http-response.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/curl-request.md")).result(binding) %> - -A `POST` request is used to create a note - -#### Request structure - -<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/request-fields.md")).result(binding) %> - - - -## Tags - -The Tags resource is used to create and list tags. - -### Listing tags - -<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/http-response.md")).result(binding) %> - -A `GET` request will list all of the service's tags. - -#### Response structure - -<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/response-fields.md")).result(binding) %> - - -### Creating a tag - -<%= ERB.new(File.read("../build/generated-snippets/tags-create-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tags-create-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/http-response.md")).result(binding) %> - -A `POST` request is used to create a tag - -#### Request structure - -<%= ERB.new(File.read("../build/generated-snippets/tags-create-example/request-fields.md")).result(binding) %> - - - -## Note - -The Note resource is used to retrieve, update, and delete individual notes - -### Retrieve a note - -<%= ERB.new(File.read("../build/generated-snippets/note-get-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/note-get-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/note-get-example/http-response.md")).result(binding) %> - -A `GET` request will retrieve the details of a note - -<%= ERB.new(File.read("../build/generated-snippets/note-get-example/response-fields.md")).result(binding) %> - -#### Links - -<%= ERB.new(File.read("../build/generated-snippets/note-get-example/links.md")).result(binding) %> - -### Update a note - -<%= ERB.new(File.read("../build/generated-snippets/note-update-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/note-update-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/note-update-example/http-response.md")).result(binding) %> - -A `PATCH` request is used to update a note - -#### Request structure - -<%= ERB.new(File.read("../build/generated-snippets/note-update-example/request-fields.md")).result(binding) %> - -To leave an attribute of a note unchanged, any of the above may be omitted from the -request. - - - -## Tag - -The Tag resource is used to retrieve, update, and delete individual tags - -### Retrieve a tag - -<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/http-response.md")).result(binding) %> - -A `GET` request will retrieve the details of a tag - -#### Response structure - -<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/response-fields.md")).result(binding) %> - -#### Links - -<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/links.md")).result(binding) %> - -### Update a tag - -<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/curl-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/http-request.md")).result(binding) %> -<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/http-response.md")).result(binding) %> - -A `PATCH` request is used to update a tag - -#### Request structure - -<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/request-fields.md")).result(binding) %> \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.eot b/samples/rest-notes-slate/slate/source/fonts/slate.eot deleted file mode 100755 index 13c4839a1..000000000 Binary files a/samples/rest-notes-slate/slate/source/fonts/slate.eot and /dev/null differ diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.svg b/samples/rest-notes-slate/slate/source/fonts/slate.svg deleted file mode 100755 index 82e48fb50..000000000 --- a/samples/rest-notes-slate/slate/source/fonts/slate.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - -Generated by IcoMoon - - - - - - - - - - diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.ttf b/samples/rest-notes-slate/slate/source/fonts/slate.ttf deleted file mode 100755 index ace9a46a7..000000000 Binary files a/samples/rest-notes-slate/slate/source/fonts/slate.ttf and /dev/null differ diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.woff b/samples/rest-notes-slate/slate/source/fonts/slate.woff deleted file mode 100755 index 1e72e0ee0..000000000 Binary files a/samples/rest-notes-slate/slate/source/fonts/slate.woff and /dev/null differ diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.woff2 b/samples/rest-notes-slate/slate/source/fonts/slate.woff2 deleted file mode 100755 index 7c585a727..000000000 Binary files a/samples/rest-notes-slate/slate/source/fonts/slate.woff2 and /dev/null differ diff --git a/samples/rest-notes-slate/slate/source/images/logo.png b/samples/rest-notes-slate/slate/source/images/logo.png deleted file mode 100644 index fa1f13da8..000000000 Binary files a/samples/rest-notes-slate/slate/source/images/logo.png and /dev/null differ diff --git a/samples/rest-notes-slate/slate/source/images/navbar.png b/samples/rest-notes-slate/slate/source/images/navbar.png deleted file mode 100644 index df38e90d8..000000000 Binary files a/samples/rest-notes-slate/slate/source/images/navbar.png and /dev/null differ diff --git a/samples/rest-notes-slate/slate/source/javascripts/all.js b/samples/rest-notes-slate/slate/source/javascripts/all.js deleted file mode 100644 index ffaa9b013..000000000 --- a/samples/rest-notes-slate/slate/source/javascripts/all.js +++ /dev/null @@ -1,4 +0,0 @@ -//= require ./lib/_energize -//= require ./app/_lang -//= require ./app/_search -//= require ./app/_toc diff --git a/samples/rest-notes-slate/slate/source/javascripts/all_nosearch.js b/samples/rest-notes-slate/slate/source/javascripts/all_nosearch.js deleted file mode 100644 index 818bc4e50..000000000 --- a/samples/rest-notes-slate/slate/source/javascripts/all_nosearch.js +++ /dev/null @@ -1,3 +0,0 @@ -//= require ./lib/_energize -//= require ./app/_lang -//= require ./app/_toc diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js b/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js deleted file mode 100644 index 9ba1aebc1..000000000 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js +++ /dev/null @@ -1,162 +0,0 @@ -/* -Copyright 2008-2013 Concur Technologies, Inc. - -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. -*/ -(function (global) { - 'use strict'; - - var languages = []; - - global.setupLanguages = setupLanguages; - global.activateLanguage = activateLanguage; - - function activateLanguage(language) { - if (!language) return; - if (language === "") return; - - $(".lang-selector a").removeClass('active'); - $(".lang-selector a[data-language-name='" + language + "']").addClass('active'); - for (var i=0; i < languages.length; i++) { - $(".highlight." + languages[i]).hide(); - } - $(".highlight." + language).show(); - - global.toc.calculateHeights(); - - // scroll to the new location of the position - if ($(window.location.hash).get(0)) { - $(window.location.hash).get(0).scrollIntoView(true); - } - } - - // parseURL and stringifyURL are from https://github.com/sindresorhus/query-string - // MIT licensed - // https://github.com/sindresorhus/query-string/blob/7bee64c16f2da1a326579e96977b9227bf6da9e6/license - function parseURL(str) { - if (typeof str !== 'string') { - return {}; - } - - str = str.trim().replace(/^(\?|#|&)/, ''); - - if (!str) { - return {}; - } - - return str.split('&').reduce(function (ret, param) { - var parts = param.replace(/\+/g, ' ').split('='); - var key = parts[0]; - var val = parts[1]; - - key = decodeURIComponent(key); - // missing `=` should be `null`: - // https://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters - val = val === undefined ? null : decodeURIComponent(val); - - if (!ret.hasOwnProperty(key)) { - ret[key] = val; - } else if (Array.isArray(ret[key])) { - ret[key].push(val); - } else { - ret[key] = [ret[key], val]; - } - - return ret; - }, {}); - }; - - function stringifyURL(obj) { - return obj ? Object.keys(obj).sort().map(function (key) { - var val = obj[key]; - - if (Array.isArray(val)) { - return val.sort().map(function (val2) { - return encodeURIComponent(key) + '=' + encodeURIComponent(val2); - }).join('&'); - } - - return encodeURIComponent(key) + '=' + encodeURIComponent(val); - }).join('&') : ''; - }; - - // gets the language set in the query string - function getLanguageFromQueryString() { - if (location.search.length >= 1) { - var language = parseURL(location.search).language - if (language) { - return language; - } else if (jQuery.inArray(location.search.substr(1), languages) != -1) { - return location.search.substr(1); - } - } - - return false; - } - - // returns a new query string with the new language in it - function generateNewQueryString(language) { - var url = parseURL(location.search); - if (url.language) { - url.language = language; - return stringifyURL(url); - } - return language; - } - - // if a button is clicked, add the state to the history - function pushURL(language) { - if (!history) { return; } - var hash = window.location.hash; - if (hash) { - hash = hash.replace(/^#+/, ''); - } - history.pushState({}, '', '?' + generateNewQueryString(language) + '#' + hash); - - // save language as next default - localStorage.setItem("language", language); - } - - function setupLanguages(l) { - var defaultLanguage = localStorage.getItem("language"); - - languages = l; - - var presetLanguage = getLanguageFromQueryString(); - if (presetLanguage) { - // the language is in the URL, so use that language! - activateLanguage(presetLanguage); - - localStorage.setItem("language", presetLanguage); - } else if ((defaultLanguage !== null) && (jQuery.inArray(defaultLanguage, languages) != -1)) { - // the language was the last selected one saved in localstorage, so use that language! - activateLanguage(defaultLanguage); - } else { - // no language selected, so use the default - activateLanguage(languages[0]); - } - } - - // if we click on a language tab, activate that language - $(function() { - $(".lang-selector a").on("click", function() { - var language = $(this).data("language-name"); - pushURL(language); - activateLanguage(language); - return false; - }); - window.onpopstate = function() { - activateLanguage(getLanguageFromQueryString()); - }; - }); -})(window); diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_search.js b/samples/rest-notes-slate/slate/source/javascripts/app/_search.js deleted file mode 100644 index 91f38a04e..000000000 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_search.js +++ /dev/null @@ -1,74 +0,0 @@ -//= require ../lib/_lunr -//= require ../lib/_jquery.highlight -(function () { - 'use strict'; - - var content, searchResults; - var highlightOpts = { element: 'span', className: 'search-highlight' }; - - var index = new lunr.Index(); - - index.ref('id'); - index.field('title', { boost: 10 }); - index.field('body'); - index.pipeline.add(lunr.trimmer, lunr.stopWordFilter); - - $(populate); - $(bind); - - function populate() { - $('h1, h2').each(function() { - var title = $(this); - var body = title.nextUntil('h1, h2'); - index.add({ - id: title.prop('id'), - title: title.text(), - body: body.text() - }); - }); - } - - function bind() { - content = $('.content'); - searchResults = $('.search-results'); - - $('#input-search').on('keyup', search); - } - - function search(event) { - unhighlight(); - searchResults.addClass('visible'); - - // ESC clears the field - if (event.keyCode === 27) this.value = ''; - - if (this.value) { - var results = index.search(this.value).filter(function(r) { - return r.score > 0.0001; - }); - - if (results.length) { - searchResults.empty(); - $.each(results, function (index, result) { - var elem = document.getElementById(result.ref); - searchResults.append("
  • " + $(elem).text() + "
  • "); - }); - highlight.call(this); - } else { - searchResults.html('
  • '); - $('.search-results li').text('No Results Found for "' + this.value + '"'); - } - } else { - unhighlight(); - searchResults.removeClass('visible'); - } - } - - function highlight() { - if (this.value) content.highlight(this.value, highlightOpts); - } - - function unhighlight() { - content.unhighlight(highlightOpts); - } -})(); diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js b/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js deleted file mode 100644 index bc2aa3e1f..000000000 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js +++ /dev/null @@ -1,55 +0,0 @@ -//= require ../lib/_jquery_ui -//= require ../lib/_jquery.tocify -//= require ../lib/_imagesloaded.min -(function (global) { - 'use strict'; - - var closeToc = function() { - $(".tocify-wrapper").removeClass('open'); - $("#nav-button").removeClass('open'); - }; - - var makeToc = function() { - global.toc = $("#toc").tocify({ - selectors: 'h1, h2', - extendPage: false, - theme: 'none', - smoothScroll: false, - showEffectSpeed: 0, - hideEffectSpeed: 180, - ignoreSelector: '.toc-ignore', - highlightOffset: 60, - scrollTo: -1, - scrollHistory: true, - hashGenerator: function (text, element) { - return element.prop('id'); - } - }).data('toc-tocify'); - - $("#nav-button").click(function() { - $(".tocify-wrapper").toggleClass('open'); - $("#nav-button").toggleClass('open'); - return false; - }); - - $(".page-wrapper").click(closeToc); - $(".tocify-item").click(closeToc); - }; - - // Hack to make already open sections to start opened, - // instead of displaying an ugly animation - function animate() { - setTimeout(function() { - toc.setOption('showEffectSpeed', 180); - }, 50); - } - - $(function() { - makeToc(); - animate(); - $('.content').imagesLoaded( function() { - global.toc.calculateHeights(); - }); - }); -})(window); - diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_energize.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_energize.js deleted file mode 100644 index 582407842..000000000 --- a/samples/rest-notes-slate/slate/source/javascripts/lib/_energize.js +++ /dev/null @@ -1,169 +0,0 @@ -/** - * energize.js v0.1.0 - * - * Speeds up click events on mobile devices. - * https://github.com/davidcalhoun/energize.js - */ - -(function() { // Sandbox - /** - * Don't add to non-touch devices, which don't need to be sped up - */ - if(!('ontouchstart' in window)) return; - - var lastClick = {}, - isThresholdReached, touchstart, touchmove, touchend, - click, closest; - - /** - * isThresholdReached - * - * Compare touchstart with touchend xy coordinates, - * and only fire simulated click event if the coordinates - * are nearby. (don't want clicking to be confused with a swipe) - */ - isThresholdReached = function(startXY, xy) { - return Math.abs(startXY[0] - xy[0]) > 5 || Math.abs(startXY[1] - xy[1]) > 5; - }; - - /** - * touchstart - * - * Save xy coordinates when the user starts touching the screen - */ - touchstart = function(e) { - this.startXY = [e.touches[0].clientX, e.touches[0].clientY]; - this.threshold = false; - }; - - /** - * touchmove - * - * Check if the user is scrolling past the threshold. - * Have to check here because touchend will not always fire - * on some tested devices (Kindle Fire?) - */ - touchmove = function(e) { - // NOOP if the threshold has already been reached - if(this.threshold) return false; - - this.threshold = isThresholdReached(this.startXY, [e.touches[0].clientX, e.touches[0].clientY]); - }; - - /** - * touchend - * - * If the user didn't scroll past the threshold between - * touchstart and touchend, fire a simulated click. - * - * (This will fire before a native click) - */ - touchend = function(e) { - // Don't fire a click if the user scrolled past the threshold - if(this.threshold || isThresholdReached(this.startXY, [e.changedTouches[0].clientX, e.changedTouches[0].clientY])) { - return; - } - - /** - * Create and fire a click event on the target element - * https://developer.mozilla.org/en/DOM/event.initMouseEvent - */ - var touch = e.changedTouches[0], - evt = document.createEvent('MouseEvents'); - evt.initMouseEvent('click', true, true, window, 0, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); - evt.simulated = true; // distinguish from a normal (nonsimulated) click - e.target.dispatchEvent(evt); - }; - - /** - * click - * - * Because we've already fired a click event in touchend, - * we need to listed for all native click events here - * and suppress them as necessary. - */ - click = function(e) { - /** - * Prevent ghost clicks by only allowing clicks we created - * in the click event we fired (look for e.simulated) - */ - var time = Date.now(), - timeDiff = time - lastClick.time, - x = e.clientX, - y = e.clientY, - xyDiff = [Math.abs(lastClick.x - x), Math.abs(lastClick.y - y)], - target = closest(e.target, 'A') || e.target, // needed for standalone apps - nodeName = target.nodeName, - isLink = nodeName === 'A', - standAlone = window.navigator.standalone && isLink && e.target.getAttribute("href"); - - lastClick.time = time; - lastClick.x = x; - lastClick.y = y; - - /** - * Unfortunately Android sometimes fires click events without touch events (seen on Kindle Fire), - * so we have to add more logic to determine the time of the last click. Not perfect... - * - * Older, simpler check: if((!e.simulated) || standAlone) - */ - if((!e.simulated && (timeDiff < 500 || (timeDiff < 1500 && xyDiff[0] < 50 && xyDiff[1] < 50))) || standAlone) { - e.preventDefault(); - e.stopPropagation(); - if(!standAlone) return false; - } - - /** - * Special logic for standalone web apps - * See https://stackoverflow.com/questions/2898740/iphone-safari-web-app-opens-links-in-new-window - */ - if(standAlone) { - window.location = target.getAttribute("href"); - } - - /** - * Add an energize-focus class to the targeted link (mimics :focus behavior) - * TODO: test and/or remove? Does this work? - */ - if(!target || !target.classList) return; - target.classList.add("energize-focus"); - window.setTimeout(function(){ - target.classList.remove("energize-focus"); - }, 150); - }; - - /** - * closest - * @param {HTMLElement} node current node to start searching from. - * @param {string} tagName the (uppercase) name of the tag you're looking for. - * - * Find the closest ancestor tag of a given node. - * - * Starts at node and goes up the DOM tree looking for a - * matching nodeName, continuing until hitting document.body - */ - closest = function(node, tagName){ - var curNode = node; - - while(curNode !== document.body) { // go up the dom until we find the tag we're after - if(!curNode || curNode.nodeName === tagName) { return curNode; } // found - curNode = curNode.parentNode; // not found, so keep going up - } - - return null; // not found - }; - - /** - * Add all delegated event listeners - * - * All the events we care about bubble up to document, - * so we can take advantage of event delegation. - * - * Note: no need to wait for DOMContentLoaded here - */ - document.addEventListener('touchstart', touchstart, false); - document.addEventListener('touchmove', touchmove, false); - document.addEventListener('touchend', touchend, false); - document.addEventListener('click', click, true); // TODO: why does this use capture? - -})(); \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_imagesloaded.min.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_imagesloaded.min.js deleted file mode 100644 index d66f65893..000000000 --- a/samples/rest-notes-slate/slate/source/javascripts/lib/_imagesloaded.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * imagesLoaded PACKAGED v3.1.8 - * JavaScript is all like "You images are done yet or what?" - * MIT License - */ - -(function(){function e(){}function t(e,t){for(var n=e.length;n--;)if(e[n].listener===t)return n;return-1}function n(e){return function(){return this[e].apply(this,arguments)}}var i=e.prototype,r=this,o=r.EventEmitter;i.getListeners=function(e){var t,n,i=this._getEvents();if("object"==typeof e){t={};for(n in i)i.hasOwnProperty(n)&&e.test(n)&&(t[n]=i[n])}else t=i[e]||(i[e]=[]);return t},i.flattenListeners=function(e){var t,n=[];for(t=0;e.length>t;t+=1)n.push(e[t].listener);return n},i.getListenersAsObject=function(e){var t,n=this.getListeners(e);return n instanceof Array&&(t={},t[e]=n),t||n},i.addListener=function(e,n){var i,r=this.getListenersAsObject(e),o="object"==typeof n;for(i in r)r.hasOwnProperty(i)&&-1===t(r[i],n)&&r[i].push(o?n:{listener:n,once:!1});return this},i.on=n("addListener"),i.addOnceListener=function(e,t){return this.addListener(e,{listener:t,once:!0})},i.once=n("addOnceListener"),i.defineEvent=function(e){return this.getListeners(e),this},i.defineEvents=function(e){for(var t=0;e.length>t;t+=1)this.defineEvent(e[t]);return this},i.removeListener=function(e,n){var i,r,o=this.getListenersAsObject(e);for(r in o)o.hasOwnProperty(r)&&(i=t(o[r],n),-1!==i&&o[r].splice(i,1));return this},i.off=n("removeListener"),i.addListeners=function(e,t){return this.manipulateListeners(!1,e,t)},i.removeListeners=function(e,t){return this.manipulateListeners(!0,e,t)},i.manipulateListeners=function(e,t,n){var i,r,o=e?this.removeListener:this.addListener,s=e?this.removeListeners:this.addListeners;if("object"!=typeof t||t instanceof RegExp)for(i=n.length;i--;)o.call(this,t,n[i]);else for(i in t)t.hasOwnProperty(i)&&(r=t[i])&&("function"==typeof r?o.call(this,i,r):s.call(this,i,r));return this},i.removeEvent=function(e){var t,n=typeof e,i=this._getEvents();if("string"===n)delete i[e];else if("object"===n)for(t in i)i.hasOwnProperty(t)&&e.test(t)&&delete i[t];else delete this._events;return this},i.removeAllListeners=n("removeEvent"),i.emitEvent=function(e,t){var n,i,r,o,s=this.getListenersAsObject(e);for(r in s)if(s.hasOwnProperty(r))for(i=s[r].length;i--;)n=s[r][i],n.once===!0&&this.removeListener(e,n.listener),o=n.listener.apply(this,t||[]),o===this._getOnceReturnValue()&&this.removeListener(e,n.listener);return this},i.trigger=n("emitEvent"),i.emit=function(e){var t=Array.prototype.slice.call(arguments,1);return this.emitEvent(e,t)},i.setOnceReturnValue=function(e){return this._onceReturnValue=e,this},i._getOnceReturnValue=function(){return this.hasOwnProperty("_onceReturnValue")?this._onceReturnValue:!0},i._getEvents=function(){return this._events||(this._events={})},e.noConflict=function(){return r.EventEmitter=o,e},"function"==typeof define&&define.amd?define("eventEmitter/EventEmitter",[],function(){return e}):"object"==typeof module&&module.exports?module.exports=e:this.EventEmitter=e}).call(this),function(e){function t(t){var n=e.event;return n.target=n.target||n.srcElement||t,n}var n=document.documentElement,i=function(){};n.addEventListener?i=function(e,t,n){e.addEventListener(t,n,!1)}:n.attachEvent&&(i=function(e,n,i){e[n+i]=i.handleEvent?function(){var n=t(e);i.handleEvent.call(i,n)}:function(){var n=t(e);i.call(e,n)},e.attachEvent("on"+n,e[n+i])});var r=function(){};n.removeEventListener?r=function(e,t,n){e.removeEventListener(t,n,!1)}:n.detachEvent&&(r=function(e,t,n){e.detachEvent("on"+t,e[t+n]);try{delete e[t+n]}catch(i){e[t+n]=void 0}});var o={bind:i,unbind:r};"function"==typeof define&&define.amd?define("eventie/eventie",o):e.eventie=o}(this),function(e,t){"function"==typeof define&&define.amd?define(["eventEmitter/EventEmitter","eventie/eventie"],function(n,i){return t(e,n,i)}):"object"==typeof exports?module.exports=t(e,require("wolfy87-eventemitter"),require("eventie")):e.imagesLoaded=t(e,e.EventEmitter,e.eventie)}(window,function(e,t,n){function i(e,t){for(var n in t)e[n]=t[n];return e}function r(e){return"[object Array]"===d.call(e)}function o(e){var t=[];if(r(e))t=e;else if("number"==typeof e.length)for(var n=0,i=e.length;i>n;n++)t.push(e[n]);else t.push(e);return t}function s(e,t,n){if(!(this instanceof s))return new s(e,t);"string"==typeof e&&(e=document.querySelectorAll(e)),this.elements=o(e),this.options=i({},this.options),"function"==typeof t?n=t:i(this.options,t),n&&this.on("always",n),this.getImages(),a&&(this.jqDeferred=new a.Deferred);var r=this;setTimeout(function(){r.check()})}function f(e){this.img=e}function c(e){this.src=e,v[e]=this}var a=e.jQuery,u=e.console,h=u!==void 0,d=Object.prototype.toString;s.prototype=new t,s.prototype.options={},s.prototype.getImages=function(){this.images=[];for(var e=0,t=this.elements.length;t>e;e++){var n=this.elements[e];"IMG"===n.nodeName&&this.addImage(n);var i=n.nodeType;if(i&&(1===i||9===i||11===i))for(var r=n.querySelectorAll("img"),o=0,s=r.length;s>o;o++){var f=r[o];this.addImage(f)}}},s.prototype.addImage=function(e){var t=new f(e);this.images.push(t)},s.prototype.check=function(){function e(e,r){return t.options.debug&&h&&u.log("confirm",e,r),t.progress(e),n++,n===i&&t.complete(),!0}var t=this,n=0,i=this.images.length;if(this.hasAnyBroken=!1,!i)return this.complete(),void 0;for(var r=0;i>r;r++){var o=this.images[r];o.on("confirm",e),o.check()}},s.prototype.progress=function(e){this.hasAnyBroken=this.hasAnyBroken||!e.isLoaded;var t=this;setTimeout(function(){t.emit("progress",t,e),t.jqDeferred&&t.jqDeferred.notify&&t.jqDeferred.notify(t,e)})},s.prototype.complete=function(){var e=this.hasAnyBroken?"fail":"done";this.isComplete=!0;var t=this;setTimeout(function(){if(t.emit(e,t),t.emit("always",t),t.jqDeferred){var n=t.hasAnyBroken?"reject":"resolve";t.jqDeferred[n](t)}})},a&&(a.fn.imagesLoaded=function(e,t){var n=new s(this,e,t);return n.jqDeferred.promise(a(this))}),f.prototype=new t,f.prototype.check=function(){var e=v[this.img.src]||new c(this.img.src);if(e.isConfirmed)return this.confirm(e.isLoaded,"cached was confirmed"),void 0;if(this.img.complete&&void 0!==this.img.naturalWidth)return this.confirm(0!==this.img.naturalWidth,"naturalWidth"),void 0;var t=this;e.on("confirm",function(e,n){return t.confirm(e.isLoaded,n),!0}),e.check()},f.prototype.confirm=function(e,t){this.isLoaded=e,this.emit("confirm",this,t)};var v={};return c.prototype=new t,c.prototype.check=function(){if(!this.isChecked){var e=new Image;n.bind(e,"load",this),n.bind(e,"error",this),e.src=this.src,this.isChecked=!0}},c.prototype.handleEvent=function(e){var t="on"+e.type;this[t]&&this[t](e)},c.prototype.onload=function(e){this.confirm(!0,"onload"),this.unbindProxyEvents(e)},c.prototype.onerror=function(e){this.confirm(!1,"onerror"),this.unbindProxyEvents(e)},c.prototype.confirm=function(e,t){this.isConfirmed=!0,this.isLoaded=e,this.emit("confirm",this,t)},c.prototype.unbindProxyEvents=function(e){n.unbind(e.target,"load",this),n.unbind(e.target,"error",this)},s}); \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.highlight.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.highlight.js deleted file mode 100644 index 6c2408608..000000000 --- a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.highlight.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * jQuery Highlight plugin - * - * Based on highlight v3 by Johann Burkard - * https://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html - * - * Code a little bit refactored and cleaned (in my humble opinion). - * Most important changes: - * - has an option to highlight only entire words (wordsOnly - false by default), - * - has an option to be case sensitive (caseSensitive - false by default) - * - highlight element tag and class names can be specified in options - * - * Usage: - * // wrap every occurrance of text 'lorem' in content - * // with (default options) - * $('#content').highlight('lorem'); - * - * // search for and highlight more terms at once - * // so you can save some time on traversing DOM - * $('#content').highlight(['lorem', 'ipsum']); - * $('#content').highlight('lorem ipsum'); - * - * // search only for entire word 'lorem' - * $('#content').highlight('lorem', { wordsOnly: true }); - * - * // don't ignore case during search of term 'lorem' - * $('#content').highlight('lorem', { caseSensitive: true }); - * - * // wrap every occurrance of term 'ipsum' in content - * // with - * $('#content').highlight('ipsum', { element: 'em', className: 'important' }); - * - * // remove default highlight - * $('#content').unhighlight(); - * - * // remove custom highlight - * $('#content').unhighlight({ element: 'em', className: 'important' }); - * - * - * Copyright (c) 2009 Bartek Szopka - * - * Licensed under MIT license. - * - */ - -jQuery.extend({ - highlight: function (node, re, nodeName, className) { - if (node.nodeType === 3) { - var match = node.data.match(re); - if (match) { - var highlight = document.createElement(nodeName || 'span'); - highlight.className = className || 'highlight'; - var wordNode = node.splitText(match.index); - wordNode.splitText(match[0].length); - var wordClone = wordNode.cloneNode(true); - highlight.appendChild(wordClone); - wordNode.parentNode.replaceChild(highlight, wordNode); - return 1; //skip added node in parent - } - } else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children - !/(script|style)/i.test(node.tagName) && // ignore script and style nodes - !(node.tagName === nodeName.toUpperCase() && node.className === className)) { // skip if already highlighted - for (var i = 0; i < node.childNodes.length; i++) { - i += jQuery.highlight(node.childNodes[i], re, nodeName, className); - } - } - return 0; - } -}); - -jQuery.fn.unhighlight = function (options) { - var settings = { className: 'highlight', element: 'span' }; - jQuery.extend(settings, options); - - return this.find(settings.element + "." + settings.className).each(function () { - var parent = this.parentNode; - parent.replaceChild(this.firstChild, this); - parent.normalize(); - }).end(); -}; - -jQuery.fn.highlight = function (words, options) { - var settings = { className: 'highlight', element: 'span', caseSensitive: false, wordsOnly: false }; - jQuery.extend(settings, options); - - if (words.constructor === String) { - words = [words]; - } - words = jQuery.grep(words, function(word, i){ - return word != ''; - }); - words = jQuery.map(words, function(word, i) { - return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); - }); - if (words.length == 0) { return this; }; - - var flag = settings.caseSensitive ? "" : "i"; - var pattern = "(" + words.join("|") + ")"; - if (settings.wordsOnly) { - pattern = "\\b" + pattern + "\\b"; - } - var re = new RegExp(pattern, flag); - - return this.each(function () { - jQuery.highlight(this, re, settings.element, settings.className); - }); -}; - diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.tocify.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.tocify.js deleted file mode 100644 index b60bc686e..000000000 --- a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.tocify.js +++ /dev/null @@ -1,1042 +0,0 @@ -/* jquery Tocify - v1.8.0 - 2013-09-16 -* http://gregfranko.com/jquery.tocify.js/ -* Copyright (c) 2013 Greg Franko; Licensed MIT -* Modified lightly by Robert Lord to fix a bug I found, -* and also so it adds ids to headers -* also because I want height caching, since the -* height lookup for h1s and h2s was causing serious -* lag spikes below 30 fps */ - -// Immediately-Invoked Function Expression (IIFE) [Ben Alman Blog Post](http://benalman.com/news/2010/11/immediately-invoked-function-expression/) that calls another IIFE that contains all of the plugin logic. I used this pattern so that anyone viewing this code would not have to scroll to the bottom of the page to view the local parameters that were passed to the main IIFE. -(function(tocify) { - - // ECMAScript 5 Strict Mode: [John Resig Blog Post](https://johnresig.com/blog/ecmascript-5-strict-mode-json-and-more/) - "use strict"; - - // Calls the second IIFE and locally passes in the global jQuery, window, and document objects - tocify(window.jQuery, window, document); - -} - -// Locally passes in `jQuery`, the `window` object, the `document` object, and an `undefined` variable. The `jQuery`, `window` and `document` objects are passed in locally, to improve performance, since javascript first searches for a variable match within the local variables set before searching the global variables set. All of the global variables are also passed in locally to be minifier friendly. `undefined` can be passed in locally, because it is not a reserved word in JavaScript. -(function($, window, document, undefined) { - - // ECMAScript 5 Strict Mode: [John Resig Blog Post](https://johnresig.com/blog/ecmascript-5-strict-mode-json-and-more/) - "use strict"; - - var tocClassName = "tocify", - tocClass = "." + tocClassName, - tocFocusClassName = "tocify-focus", - tocHoverClassName = "tocify-hover", - hideTocClassName = "tocify-hide", - hideTocClass = "." + hideTocClassName, - headerClassName = "tocify-header", - headerClass = "." + headerClassName, - subheaderClassName = "tocify-subheader", - subheaderClass = "." + subheaderClassName, - itemClassName = "tocify-item", - itemClass = "." + itemClassName, - extendPageClassName = "tocify-extend-page", - extendPageClass = "." + extendPageClassName; - - // Calling the jQueryUI Widget Factory Method - $.widget("toc.tocify", { - - //Plugin version - version: "1.8.0", - - // These options will be used as defaults - options: { - - // **context**: Accepts String: Any jQuery selector - // The container element that holds all of the elements used to generate the table of contents - context: "body", - - // **ignoreSelector**: Accepts String: Any jQuery selector - // A selector to any element that would be matched by selectors that you wish to be ignored - ignoreSelector: null, - - // **selectors**: Accepts an Array of Strings: Any jQuery selectors - // The element's used to generate the table of contents. The order is very important since it will determine the table of content's nesting structure - selectors: "h1, h2, h3", - - // **showAndHide**: Accepts a boolean: true or false - // Used to determine if elements should be shown and hidden - showAndHide: true, - - // **showEffect**: Accepts String: "none", "fadeIn", "show", or "slideDown" - // Used to display any of the table of contents nested items - showEffect: "slideDown", - - // **showEffectSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast" - // The time duration of the show animation - showEffectSpeed: "medium", - - // **hideEffect**: Accepts String: "none", "fadeOut", "hide", or "slideUp" - // Used to hide any of the table of contents nested items - hideEffect: "slideUp", - - // **hideEffectSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast" - // The time duration of the hide animation - hideEffectSpeed: "medium", - - // **smoothScroll**: Accepts a boolean: true or false - // Determines if a jQuery animation should be used to scroll to specific table of contents items on the page - smoothScroll: true, - - // **smoothScrollSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast" - // The time duration of the smoothScroll animation - smoothScrollSpeed: "medium", - - // **scrollTo**: Accepts Number (pixels) - // The amount of space between the top of page and the selected table of contents item after the page has been scrolled - scrollTo: 0, - - // **showAndHideOnScroll**: Accepts a boolean: true or false - // Determines if table of contents nested items should be shown and hidden while scrolling - showAndHideOnScroll: true, - - // **highlightOnScroll**: Accepts a boolean: true or false - // Determines if table of contents nested items should be highlighted (set to a different color) while scrolling - highlightOnScroll: true, - - // **highlightOffset**: Accepts a number - // The offset distance in pixels to trigger the next active table of contents item - highlightOffset: 40, - - // **theme**: Accepts a string: "bootstrap", "jqueryui", or "none" - // Determines if Twitter Bootstrap, jQueryUI, or Tocify classes should be added to the table of contents - theme: "bootstrap", - - // **extendPage**: Accepts a boolean: true or false - // If a user scrolls to the bottom of the page and the page is not tall enough to scroll to the last table of contents item, then the page height is increased - extendPage: true, - - // **extendPageOffset**: Accepts a number: pixels - // How close to the bottom of the page a user must scroll before the page is extended - extendPageOffset: 100, - - // **history**: Accepts a boolean: true or false - // Adds a hash to the page url to maintain history - history: true, - - // **scrollHistory**: Accepts a boolean: true or false - // Adds a hash to the page url, to maintain history, when scrolling to a TOC item - scrollHistory: false, - - // **hashGenerator**: How the hash value (the anchor segment of the URL, following the - // # character) will be generated. - // - // "compact" (default) - #CompressesEverythingTogether - // "pretty" - #looks-like-a-nice-url-and-is-easily-readable - // function(text, element){} - Your own hash generation function that accepts the text as an - // argument, and returns the hash value. - hashGenerator: "compact", - - // **highlightDefault**: Accepts a boolean: true or false - // Set's the first TOC item as active if no other TOC item is active. - highlightDefault: true - - }, - - // _Create - // ------- - // Constructs the plugin. Only called once. - _create: function() { - - var self = this; - - self.tocifyWrapper = $('.tocify-wrapper'); - self.extendPageScroll = true; - - // Internal array that keeps track of all TOC items (Helps to recognize if there are duplicate TOC item strings) - self.items = []; - - // Generates the HTML for the dynamic table of contents - self._generateToc(); - - // Caches heights and anchors - self.cachedHeights = [], - self.cachedAnchors = []; - - // Adds CSS classes to the newly generated table of contents HTML - self._addCSSClasses(); - - self.webkit = (function() { - - for(var prop in window) { - - if(prop) { - - if(prop.toLowerCase().indexOf("webkit") !== -1) { - - return true; - - } - - } - - } - - return false; - - }()); - - // Adds jQuery event handlers to the newly generated table of contents - self._setEventHandlers(); - - // Binding to the Window load event to make sure the correct scrollTop is calculated - $(window).load(function() { - - // Sets the active TOC item - self._setActiveElement(true); - - // Once all animations on the page are complete, this callback function will be called - $("html, body").promise().done(function() { - - setTimeout(function() { - - self.extendPageScroll = false; - - },0); - - }); - - }); - - }, - - // _generateToc - // ------------ - // Generates the HTML for the dynamic table of contents - _generateToc: function() { - - // _Local variables_ - - // Stores the plugin context in the self variable - var self = this, - - // All of the HTML tags found within the context provided (i.e. body) that match the top level jQuery selector above - firstElem, - - // Instantiated variable that will store the top level newly created unordered list DOM element - ul, - ignoreSelector = self.options.ignoreSelector; - - // If the selectors option has a comma within the string - if(this.options.selectors.indexOf(",") !== -1) { - - // Grabs the first selector from the string - firstElem = $(this.options.context).find(this.options.selectors.replace(/ /g,"").substr(0, this.options.selectors.indexOf(","))); - - } - - // If the selectors option does not have a comman within the string - else { - - // Grabs the first selector from the string and makes sure there are no spaces - firstElem = $(this.options.context).find(this.options.selectors.replace(/ /g,"")); - - } - - if(!firstElem.length) { - - self.element.addClass(hideTocClassName); - - return; - - } - - self.element.addClass(tocClassName); - - // Loops through each top level selector - firstElem.each(function(index) { - - //If the element matches the ignoreSelector then we skip it - if($(this).is(ignoreSelector)) { - return; - } - - // Creates an unordered list HTML element and adds a dynamic ID and standard class name - ul = $("
      ", { - "id": headerClassName + index, - "class": headerClassName - }). - - // Appends a top level list item HTML element to the previously created HTML header - append(self._nestElements($(this), index)); - - // Add the created unordered list element to the HTML element calling the plugin - self.element.append(ul); - - // Finds all of the HTML tags between the header and subheader elements - $(this).nextUntil(this.nodeName.toLowerCase()).each(function() { - - // If there are no nested subheader elemements - if($(this).find(self.options.selectors).length === 0) { - - // Loops through all of the subheader elements - $(this).filter(self.options.selectors).each(function() { - - //If the element matches the ignoreSelector then we skip it - if($(this).is(ignoreSelector)) { - return; - } - - self._appendSubheaders.call(this, self, ul); - - }); - - } - - // If there are nested subheader elements - else { - - // Loops through all of the subheader elements - $(this).find(self.options.selectors).each(function() { - - //If the element matches the ignoreSelector then we skip it - if($(this).is(ignoreSelector)) { - return; - } - - self._appendSubheaders.call(this, self, ul); - - }); - - } - - }); - - }); - - }, - - _setActiveElement: function(pageload) { - - var self = this, - - hash = window.location.hash.substring(1), - - elem = self.element.find("li[data-unique='" + hash + "']"); - - if(hash.length) { - - // Removes highlighting from all of the list item's - self.element.find("." + self.focusClass).removeClass(self.focusClass); - - // Highlights the current list item that was clicked - elem.addClass(self.focusClass); - - // If the showAndHide option is true - if(self.options.showAndHide) { - - // Triggers the click event on the currently focused TOC item - elem.click(); - - } - - } - - else { - - // Removes highlighting from all of the list item's - self.element.find("." + self.focusClass).removeClass(self.focusClass); - - if(!hash.length && pageload && self.options.highlightDefault) { - - // Highlights the first TOC item if no other items are highlighted - self.element.find(itemClass).first().addClass(self.focusClass); - - } - - } - - return self; - - }, - - // _nestElements - // ------------- - // Helps create the table of contents list by appending nested list items - _nestElements: function(self, index) { - - var arr, item, hashValue; - - arr = $.grep(this.items, function (item) { - - return item === self.text(); - - }); - - // If there is already a duplicate TOC item - if(arr.length) { - - // Adds the current TOC item text and index (for slight randomization) to the internal array - this.items.push(self.text() + index); - - } - - // If there not a duplicate TOC item - else { - - // Adds the current TOC item text to the internal array - this.items.push(self.text()); - - } - - hashValue = this._generateHashValue(arr, self, index); - - // ADDED BY ROBERT - // actually add the hash value to the element's id - // self.attr("id", "link-" + hashValue); - - // Appends a list item HTML element to the last unordered list HTML element found within the HTML element calling the plugin - item = $("
    • ", { - - // Sets a common class name to the list item - "class": itemClassName, - - "data-unique": hashValue - - }).append($("", { - - "text": self.text() - - })); - - // Adds an HTML anchor tag before the currently traversed HTML element - self.before($("
      ", { - - // Sets a name attribute on the anchor tag to the text of the currently traversed HTML element (also making sure that all whitespace is replaced with an underscore) - "name": hashValue, - - "data-unique": hashValue - - })); - - return item; - - }, - - // _generateHashValue - // ------------------ - // Generates the hash value that will be used to refer to each item. - _generateHashValue: function(arr, self, index) { - - var hashValue = "", - hashGeneratorOption = this.options.hashGenerator; - - if (hashGeneratorOption === "pretty") { - // remove weird characters - - - // prettify the text - hashValue = self.text().toLowerCase().replace(/\s/g, "-"); - - // ADDED BY ROBERT - // remove weird characters - hashValue = hashValue.replace(/[^\x00-\x7F]/g, ""); - - // fix double hyphens - while (hashValue.indexOf("--") > -1) { - hashValue = hashValue.replace(/--/g, "-"); - } - - // fix colon-space instances - while (hashValue.indexOf(":-") > -1) { - hashValue = hashValue.replace(/:-/g, "-"); - } - - } else if (typeof hashGeneratorOption === "function") { - - // call the function - hashValue = hashGeneratorOption(self.text(), self); - - } else { - - // compact - the default - hashValue = self.text().replace(/\s/g, ""); - - } - - // add the index if we need to - if (arr.length) { hashValue += ""+index; } - - // return the value - return hashValue; - - }, - - // _appendElements - // --------------- - // Helps create the table of contents list by appending subheader elements - - _appendSubheaders: function(self, ul) { - - // The current element index - var index = $(this).index(self.options.selectors), - - // Finds the previous header DOM element - previousHeader = $(self.options.selectors).eq(index - 1), - - currentTagName = +$(this).prop("tagName").charAt(1), - - previousTagName = +previousHeader.prop("tagName").charAt(1), - - lastSubheader; - - // If the current header DOM element is smaller than the previous header DOM element or the first subheader - if(currentTagName < previousTagName) { - - // Selects the last unordered list HTML found within the HTML element calling the plugin - self.element.find(subheaderClass + "[data-tag=" + currentTagName + "]").last().append(self._nestElements($(this), index)); - - } - - // If the current header DOM element is the same type of header(eg. h4) as the previous header DOM element - else if(currentTagName === previousTagName) { - - ul.find(itemClass).last().after(self._nestElements($(this), index)); - - } - - else { - - // Selects the last unordered list HTML found within the HTML element calling the plugin - ul.find(itemClass).last(). - - // Appends an unorderedList HTML element to the dynamic `unorderedList` variable and sets a common class name - after($("
        ", { - - "class": subheaderClassName, - - "data-tag": currentTagName - - })).next(subheaderClass). - - // Appends a list item HTML element to the last unordered list HTML element found within the HTML element calling the plugin - append(self._nestElements($(this), index)); - } - - }, - - // _setEventHandlers - // ---------------- - // Adds jQuery event handlers to the newly generated table of contents - _setEventHandlers: function() { - - // _Local variables_ - - // Stores the plugin context in the self variable - var self = this, - - // Instantiates a new variable that will be used to hold a specific element's context - $self, - - // Instantiates a new variable that will be used to determine the smoothScroll animation time duration - duration; - - // Event delegation that looks for any clicks on list item elements inside of the HTML element calling the plugin - this.element.on("click.tocify", "li", function(event) { - - if(self.options.history) { - - window.location.hash = $(this).attr("data-unique"); - - } - - // Removes highlighting from all of the list item's - self.element.find("." + self.focusClass).removeClass(self.focusClass); - - // Highlights the current list item that was clicked - $(this).addClass(self.focusClass); - - // If the showAndHide option is true - if(self.options.showAndHide) { - - var elem = $('li[data-unique="' + $(this).attr("data-unique") + '"]'); - - self._triggerShow(elem); - - } - - self._scrollTo($(this)); - - }); - - // Mouseenter and Mouseleave event handlers for the list item's within the HTML element calling the plugin - this.element.find("li").on({ - - // Mouseenter event handler - "mouseenter.tocify": function() { - - // Adds a hover CSS class to the current list item - $(this).addClass(self.hoverClass); - - // Makes sure the cursor is set to the pointer icon - $(this).css("cursor", "pointer"); - - }, - - // Mouseleave event handler - "mouseleave.tocify": function() { - - if(self.options.theme !== "bootstrap") { - - // Removes the hover CSS class from the current list item - $(this).removeClass(self.hoverClass); - - } - - } - }); - - // Reset height cache on scroll - - $(window).on('resize', function() { - self.calculateHeights(); - }); - - // Window scroll event handler - $(window).on("scroll.tocify", function() { - - // Once all animations on the page are complete, this callback function will be called - $("html, body").promise().done(function() { - - // Local variables - - // Stores how far the user has scrolled - var winScrollTop = $(window).scrollTop(), - - // Stores the height of the window - winHeight = $(window).height(), - - // Stores the height of the document - docHeight = $(document).height(), - - scrollHeight = $("body")[0].scrollHeight, - - // Instantiates a variable that will be used to hold a selected HTML element - elem, - - lastElem, - - lastElemOffset, - - currentElem; - - if(self.options.extendPage) { - - // If the user has scrolled to the bottom of the page and the last toc item is not focused - if((self.webkit && winScrollTop >= scrollHeight - winHeight - self.options.extendPageOffset) || (!self.webkit && winHeight + winScrollTop > docHeight - self.options.extendPageOffset)) { - - if(!$(extendPageClass).length) { - - lastElem = $('div[data-unique="' + $(itemClass).last().attr("data-unique") + '"]'); - - if(!lastElem.length) return; - - // Gets the top offset of the page header that is linked to the last toc item - lastElemOffset = lastElem.offset().top; - - // Appends a div to the bottom of the page and sets the height to the difference of the window scrollTop and the last element's position top offset - $(self.options.context).append($("
        ", { - - "class": extendPageClassName, - - "height": Math.abs(lastElemOffset - winScrollTop) + "px", - - "data-unique": extendPageClassName - - })); - - if(self.extendPageScroll) { - - currentElem = self.element.find('li.active'); - - self._scrollTo($("div[data-unique=" + currentElem.attr("data-unique") + "]")); - - } - - } - - } - - } - - // The zero timeout ensures the following code is run after the scroll events - setTimeout(function() { - - // _Local variables_ - - // Stores the distance to the closest anchor - var // Stores the index of the closest anchor - closestAnchorIdx = null, - anchorText; - - // if never calculated before, calculate and cache the heights - if (self.cachedHeights.length == 0) { - self.calculateHeights(); - } - - var scrollTop = $(window).scrollTop(); - - // Determines the index of the closest anchor - self.cachedAnchors.each(function(idx) { - if (self.cachedHeights[idx] - scrollTop < 0) { - closestAnchorIdx = idx; - } else { - return false; - } - }); - - anchorText = $(self.cachedAnchors[closestAnchorIdx]).attr("data-unique"); - - // Stores the list item HTML element that corresponds to the currently traversed anchor tag - elem = $('li[data-unique="' + anchorText + '"]'); - - // If the `highlightOnScroll` option is true and a next element is found - if(self.options.highlightOnScroll && elem.length && !elem.hasClass(self.focusClass)) { - - // Removes highlighting from all of the list item's - self.element.find("." + self.focusClass).removeClass(self.focusClass); - - // Highlights the corresponding list item - elem.addClass(self.focusClass); - - // Scroll to highlighted element's header - var tocifyWrapper = self.tocifyWrapper; - var scrollToElem = $(elem).closest('.tocify-header'); - - var elementOffset = scrollToElem.offset().top, - wrapperOffset = tocifyWrapper.offset().top; - var offset = elementOffset - wrapperOffset; - - if (offset >= $(window).height()) { - var scrollPosition = offset + tocifyWrapper.scrollTop(); - tocifyWrapper.scrollTop(scrollPosition); - } else if (offset < 0) { - tocifyWrapper.scrollTop(0); - } - } - - if(self.options.scrollHistory) { - - // IF STATEMENT ADDED BY ROBERT - - if(window.location.hash !== "#" + anchorText && anchorText !== undefined) { - - if(history.replaceState) { - history.replaceState({}, "", "#" + anchorText); - // provide a fallback - } else { - scrollV = document.body.scrollTop; - scrollH = document.body.scrollLeft; - location.hash = "#" + anchorText; - document.body.scrollTop = scrollV; - document.body.scrollLeft = scrollH; - } - - } - - } - - // If the `showAndHideOnScroll` option is true - if(self.options.showAndHideOnScroll && self.options.showAndHide) { - - self._triggerShow(elem, true); - - } - - }, 0); - - }); - - }); - - }, - - // calculateHeights - // ---- - // ADDED BY ROBERT - calculateHeights: function() { - var self = this; - self.cachedHeights = []; - self.cachedAnchors = []; - var anchors = $(self.options.context).find("div[data-unique]"); - anchors.each(function(idx) { - var distance = (($(this).next().length ? $(this).next() : $(this)).offset().top - self.options.highlightOffset); - self.cachedHeights[idx] = distance; - }); - self.cachedAnchors = anchors; - }, - - // Show - // ---- - // Opens the current sub-header - show: function(elem, scroll) { - - // Stores the plugin context in the `self` variable - var self = this, - element = elem; - - // If the sub-header is not already visible - if (!elem.is(":visible")) { - - // If the current element does not have any nested subheaders, is not a header, and its parent is not visible - if(!elem.find(subheaderClass).length && !elem.parent().is(headerClass) && !elem.parent().is(":visible")) { - - // Sets the current element to all of the subheaders within the current header - elem = elem.parents(subheaderClass).add(elem); - - } - - // If the current element does not have any nested subheaders and is not a header - else if(!elem.children(subheaderClass).length && !elem.parent().is(headerClass)) { - - // Sets the current element to the closest subheader - elem = elem.closest(subheaderClass); - - } - - //Determines what jQuery effect to use - switch (self.options.showEffect) { - - //Uses `no effect` - case "none": - - elem.show(); - - break; - - //Uses the jQuery `show` special effect - case "show": - - elem.show(self.options.showEffectSpeed); - - break; - - //Uses the jQuery `slideDown` special effect - case "slideDown": - - elem.slideDown(self.options.showEffectSpeed); - - break; - - //Uses the jQuery `fadeIn` special effect - case "fadeIn": - - elem.fadeIn(self.options.showEffectSpeed); - - break; - - //If none of the above options were passed, then a `jQueryUI show effect` is expected - default: - - elem.show(); - - break; - - } - - } - - // If the current subheader parent element is a header - if(elem.parent().is(headerClass)) { - - // Hides all non-active sub-headers - self.hide($(subheaderClass).not(elem)); - - } - - // If the current subheader parent element is not a header - else { - - // Hides all non-active sub-headers - self.hide($(subheaderClass).not(elem.closest(headerClass).find(subheaderClass).not(elem.siblings()))); - - } - - // Maintains chainablity - return self; - - }, - - // Hide - // ---- - // Closes the current sub-header - hide: function(elem) { - - // Stores the plugin context in the `self` variable - var self = this; - - //Determines what jQuery effect to use - switch (self.options.hideEffect) { - - // Uses `no effect` - case "none": - - elem.hide(); - - break; - - // Uses the jQuery `hide` special effect - case "hide": - - elem.hide(self.options.hideEffectSpeed); - - break; - - // Uses the jQuery `slideUp` special effect - case "slideUp": - - elem.slideUp(self.options.hideEffectSpeed); - - break; - - // Uses the jQuery `fadeOut` special effect - case "fadeOut": - - elem.fadeOut(self.options.hideEffectSpeed); - - break; - - // If none of the above options were passed, then a `jqueryUI hide effect` is expected - default: - - elem.hide(); - - break; - - } - - // Maintains chainablity - return self; - }, - - // _triggerShow - // ------------ - // Determines what elements get shown on scroll and click - _triggerShow: function(elem, scroll) { - - var self = this; - - // If the current element's parent is a header element or the next element is a nested subheader element - if(elem.parent().is(headerClass) || elem.next().is(subheaderClass)) { - - // Shows the next sub-header element - self.show(elem.next(subheaderClass), scroll); - - } - - // If the current element's parent is a subheader element - else if(elem.parent().is(subheaderClass)) { - - // Shows the parent sub-header element - self.show(elem.parent(), scroll); - - } - - // Maintains chainability - return self; - - }, - - // _addCSSClasses - // -------------- - // Adds CSS classes to the newly generated table of contents HTML - _addCSSClasses: function() { - - // If the user wants a jqueryUI theme - if(this.options.theme === "jqueryui") { - - this.focusClass = "ui-state-default"; - - this.hoverClass = "ui-state-hover"; - - //Adds the default styling to the dropdown list - this.element.addClass("ui-widget").find(".toc-title").addClass("ui-widget-header").end().find("li").addClass("ui-widget-content"); - - } - - // If the user wants a twitterBootstrap theme - else if(this.options.theme === "bootstrap") { - - this.element.find(headerClass + "," + subheaderClass).addClass("nav nav-list"); - - this.focusClass = "active"; - - } - - // If a user does not want a prebuilt theme - else { - - // Adds more neutral classes (instead of jqueryui) - - this.focusClass = tocFocusClassName; - - this.hoverClass = tocHoverClassName; - - } - - //Maintains chainability - return this; - - }, - - // setOption - // --------- - // Sets a single Tocify option after the plugin is invoked - setOption: function() { - - // Calls the jQueryUI Widget Factory setOption method - $.Widget.prototype._setOption.apply(this, arguments); - - }, - - // setOptions - // ---------- - // Sets a single or multiple Tocify options after the plugin is invoked - setOptions: function() { - - // Calls the jQueryUI Widget Factory setOptions method - $.Widget.prototype._setOptions.apply(this, arguments); - - }, - - // _scrollTo - // --------- - // Scrolls to a specific element - _scrollTo: function(elem) { - - var self = this, - duration = self.options.smoothScroll || 0, - scrollTo = self.options.scrollTo; - - // Once all animations on the page are complete, this callback function will be called - $("html, body").promise().done(function() { - - // Animates the html and body element scrolltops - $("html, body").animate({ - - // Sets the jQuery `scrollTop` to the top offset of the HTML div tag that matches the current list item's `data-unique` tag - "scrollTop": $('div[data-unique="' + elem.attr("data-unique") + '"]').next().offset().top - ($.isFunction(scrollTo) ? scrollTo.call() : scrollTo) + "px" - - }, { - - // Sets the smoothScroll animation time duration to the smoothScrollSpeed option - "duration": duration - - }); - - }); - - // Maintains chainability - return self; - - } - - }); - -})); //end of plugin diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery_ui.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery_ui.js deleted file mode 100644 index 2417b46e6..000000000 --- a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery_ui.js +++ /dev/null @@ -1,566 +0,0 @@ -/*! jQuery UI - v1.11.3 - 2015-02-12 - * https://jqueryui.com - * Includes: widget.js - * Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ - -(function( factory ) { - if ( typeof define === "function" && define.amd ) { - - // AMD. Register as an anonymous module. - define([ "jquery" ], factory ); - } else { - - // Browser globals - factory( jQuery ); - } -}(function( $ ) { - /*! - * jQuery UI Widget 1.11.3 - * https://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * https://jquery.org/license - * - * https://api.jqueryui.com/jQuery.widget/ - */ - - - var widget_uuid = 0, - widget_slice = Array.prototype.slice; - - $.cleanData = (function( orig ) { - return function( elems ) { - var events, elem, i; - for ( i = 0; (elem = elems[i]) != null; i++ ) { - try { - - // Only trigger remove when necessary to save time - events = $._data( elem, "events" ); - if ( events && events.remove ) { - $( elem ).triggerHandler( "remove" ); - } - - // https://bugs.jquery.com/ticket/8235 - } catch ( e ) {} - } - orig( elems ); - }; - })( $.cleanData ); - - $.widget = function( name, base, prototype ) { - var fullName, existingConstructor, constructor, basePrototype, - // proxiedPrototype allows the provided prototype to remain unmodified - // so that it can be used as a mixin for multiple widgets (#8876) - proxiedPrototype = {}, - namespace = name.split( "." )[ 0 ]; - - name = name.split( "." )[ 1 ]; - fullName = namespace + "-" + name; - - if ( !prototype ) { - prototype = base; - base = $.Widget; - } - - // create selector for plugin - $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { - return !!$.data( elem, fullName ); - }; - - $[ namespace ] = $[ namespace ] || {}; - existingConstructor = $[ namespace ][ name ]; - constructor = $[ namespace ][ name ] = function( options, element ) { - // allow instantiation without "new" keyword - if ( !this._createWidget ) { - return new constructor( options, element ); - } - - // allow instantiation without initializing for simple inheritance - // must use "new" keyword (the code above always passes args) - if ( arguments.length ) { - this._createWidget( options, element ); - } - }; - // extend with the existing constructor to carry over any static properties - $.extend( constructor, existingConstructor, { - version: prototype.version, - // copy the object used to create the prototype in case we need to - // redefine the widget later - _proto: $.extend( {}, prototype ), - // track widgets that inherit from this widget in case this widget is - // redefined after a widget inherits from it - _childConstructors: [] - }); - - basePrototype = new base(); - // we need to make the options hash a property directly on the new instance - // otherwise we'll modify the options hash on the prototype that we're - // inheriting from - basePrototype.options = $.widget.extend( {}, basePrototype.options ); - $.each( prototype, function( prop, value ) { - if ( !$.isFunction( value ) ) { - proxiedPrototype[ prop ] = value; - return; - } - proxiedPrototype[ prop ] = (function() { - var _super = function() { - return base.prototype[ prop ].apply( this, arguments ); - }, - _superApply = function( args ) { - return base.prototype[ prop ].apply( this, args ); - }; - return function() { - var __super = this._super, - __superApply = this._superApply, - returnValue; - - this._super = _super; - this._superApply = _superApply; - - returnValue = value.apply( this, arguments ); - - this._super = __super; - this._superApply = __superApply; - - return returnValue; - }; - })(); - }); - constructor.prototype = $.widget.extend( basePrototype, { - // TODO: remove support for widgetEventPrefix - // always use the name + a colon as the prefix, e.g., draggable:start - // don't prefix for widgets that aren't DOM-based - widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name - }, proxiedPrototype, { - constructor: constructor, - namespace: namespace, - widgetName: name, - widgetFullName: fullName - }); - - // If this widget is being redefined then we need to find all widgets that - // are inheriting from it and redefine all of them so that they inherit from - // the new version of this widget. We're essentially trying to replace one - // level in the prototype chain. - if ( existingConstructor ) { - $.each( existingConstructor._childConstructors, function( i, child ) { - var childPrototype = child.prototype; - - // redefine the child widget using the same prototype that was - // originally used, but inherit from the new version of the base - $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); - }); - // remove the list of existing child constructors from the old constructor - // so the old child constructors can be garbage collected - delete existingConstructor._childConstructors; - } else { - base._childConstructors.push( constructor ); - } - - $.widget.bridge( name, constructor ); - - return constructor; - }; - - $.widget.extend = function( target ) { - var input = widget_slice.call( arguments, 1 ), - inputIndex = 0, - inputLength = input.length, - key, - value; - for ( ; inputIndex < inputLength; inputIndex++ ) { - for ( key in input[ inputIndex ] ) { - value = input[ inputIndex ][ key ]; - if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { - // Clone objects - if ( $.isPlainObject( value ) ) { - target[ key ] = $.isPlainObject( target[ key ] ) ? - $.widget.extend( {}, target[ key ], value ) : - // Don't extend strings, arrays, etc. with objects - $.widget.extend( {}, value ); - // Copy everything else by reference - } else { - target[ key ] = value; - } - } - } - } - return target; - }; - - $.widget.bridge = function( name, object ) { - var fullName = object.prototype.widgetFullName || name; - $.fn[ name ] = function( options ) { - var isMethodCall = typeof options === "string", - args = widget_slice.call( arguments, 1 ), - returnValue = this; - - if ( isMethodCall ) { - this.each(function() { - var methodValue, - instance = $.data( this, fullName ); - if ( options === "instance" ) { - returnValue = instance; - return false; - } - if ( !instance ) { - return $.error( "cannot call methods on " + name + " prior to initialization; " + - "attempted to call method '" + options + "'" ); - } - if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { - return $.error( "no such method '" + options + "' for " + name + " widget instance" ); - } - methodValue = instance[ options ].apply( instance, args ); - if ( methodValue !== instance && methodValue !== undefined ) { - returnValue = methodValue && methodValue.jquery ? - returnValue.pushStack( methodValue.get() ) : - methodValue; - return false; - } - }); - } else { - - // Allow multiple hashes to be passed on init - if ( args.length ) { - options = $.widget.extend.apply( null, [ options ].concat(args) ); - } - - this.each(function() { - var instance = $.data( this, fullName ); - if ( instance ) { - instance.option( options || {} ); - if ( instance._init ) { - instance._init(); - } - } else { - $.data( this, fullName, new object( options, this ) ); - } - }); - } - - return returnValue; - }; - }; - - $.Widget = function( /* options, element */ ) {}; - $.Widget._childConstructors = []; - - $.Widget.prototype = { - widgetName: "widget", - widgetEventPrefix: "", - defaultElement: "
        ", - options: { - disabled: false, - - // callbacks - create: null - }, - _createWidget: function( options, element ) { - element = $( element || this.defaultElement || this )[ 0 ]; - this.element = $( element ); - this.uuid = widget_uuid++; - this.eventNamespace = "." + this.widgetName + this.uuid; - - this.bindings = $(); - this.hoverable = $(); - this.focusable = $(); - - if ( element !== this ) { - $.data( element, this.widgetFullName, this ); - this._on( true, this.element, { - remove: function( event ) { - if ( event.target === element ) { - this.destroy(); - } - } - }); - this.document = $( element.style ? - // element within the document - element.ownerDocument : - // element is window or document - element.document || element ); - this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); - } - - this.options = $.widget.extend( {}, - this.options, - this._getCreateOptions(), - options ); - - this._create(); - this._trigger( "create", null, this._getCreateEventData() ); - this._init(); - }, - _getCreateOptions: $.noop, - _getCreateEventData: $.noop, - _create: $.noop, - _init: $.noop, - - destroy: function() { - this._destroy(); - // we can probably remove the unbind calls in 2.0 - // all event bindings should go through this._on() - this.element - .unbind( this.eventNamespace ) - .removeData( this.widgetFullName ) - // support: jquery <1.6.3 - // https://bugs.jquery.com/ticket/9413 - .removeData( $.camelCase( this.widgetFullName ) ); - this.widget() - .unbind( this.eventNamespace ) - .removeAttr( "aria-disabled" ) - .removeClass( - this.widgetFullName + "-disabled " + - "ui-state-disabled" ); - - // clean up events and states - this.bindings.unbind( this.eventNamespace ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - }, - _destroy: $.noop, - - widget: function() { - return this.element; - }, - - option: function( key, value ) { - var options = key, - parts, - curOption, - i; - - if ( arguments.length === 0 ) { - // don't return a reference to the internal hash - return $.widget.extend( {}, this.options ); - } - - if ( typeof key === "string" ) { - // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } - options = {}; - parts = key.split( "." ); - key = parts.shift(); - if ( parts.length ) { - curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); - for ( i = 0; i < parts.length - 1; i++ ) { - curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; - curOption = curOption[ parts[ i ] ]; - } - key = parts.pop(); - if ( arguments.length === 1 ) { - return curOption[ key ] === undefined ? null : curOption[ key ]; - } - curOption[ key ] = value; - } else { - if ( arguments.length === 1 ) { - return this.options[ key ] === undefined ? null : this.options[ key ]; - } - options[ key ] = value; - } - } - - this._setOptions( options ); - - return this; - }, - _setOptions: function( options ) { - var key; - - for ( key in options ) { - this._setOption( key, options[ key ] ); - } - - return this; - }, - _setOption: function( key, value ) { - this.options[ key ] = value; - - if ( key === "disabled" ) { - this.widget() - .toggleClass( this.widgetFullName + "-disabled", !!value ); - - // If the widget is becoming disabled, then nothing is interactive - if ( value ) { - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - } - } - - return this; - }, - - enable: function() { - return this._setOptions({ disabled: false }); - }, - disable: function() { - return this._setOptions({ disabled: true }); - }, - - _on: function( suppressDisabledCheck, element, handlers ) { - var delegateElement, - instance = this; - - // no suppressDisabledCheck flag, shuffle arguments - if ( typeof suppressDisabledCheck !== "boolean" ) { - handlers = element; - element = suppressDisabledCheck; - suppressDisabledCheck = false; - } - - // no element argument, shuffle and use this.element - if ( !handlers ) { - handlers = element; - element = this.element; - delegateElement = this.widget(); - } else { - element = delegateElement = $( element ); - this.bindings = this.bindings.add( element ); - } - - $.each( handlers, function( event, handler ) { - function handlerProxy() { - // allow widgets to customize the disabled handling - // - disabled as an array instead of boolean - // - disabled class as method for disabling individual parts - if ( !suppressDisabledCheck && - ( instance.options.disabled === true || - $( this ).hasClass( "ui-state-disabled" ) ) ) { - return; - } - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - - // copy the guid so direct unbinding works - if ( typeof handler !== "string" ) { - handlerProxy.guid = handler.guid = - handler.guid || handlerProxy.guid || $.guid++; - } - - var match = event.match( /^([\w:-]*)\s*(.*)$/ ), - eventName = match[1] + instance.eventNamespace, - selector = match[2]; - if ( selector ) { - delegateElement.delegate( selector, eventName, handlerProxy ); - } else { - element.bind( eventName, handlerProxy ); - } - }); - }, - - _off: function( element, eventName ) { - eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + - this.eventNamespace; - element.unbind( eventName ).undelegate( eventName ); - - // Clear the stack to avoid memory leaks (#10056) - this.bindings = $( this.bindings.not( element ).get() ); - this.focusable = $( this.focusable.not( element ).get() ); - this.hoverable = $( this.hoverable.not( element ).get() ); - }, - - _delay: function( handler, delay ) { - function handlerProxy() { - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - var instance = this; - return setTimeout( handlerProxy, delay || 0 ); - }, - - _hoverable: function( element ) { - this.hoverable = this.hoverable.add( element ); - this._on( element, { - mouseenter: function( event ) { - $( event.currentTarget ).addClass( "ui-state-hover" ); - }, - mouseleave: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-hover" ); - } - }); - }, - - _focusable: function( element ) { - this.focusable = this.focusable.add( element ); - this._on( element, { - focusin: function( event ) { - $( event.currentTarget ).addClass( "ui-state-focus" ); - }, - focusout: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-focus" ); - } - }); - }, - - _trigger: function( type, event, data ) { - var prop, orig, - callback = this.options[ type ]; - - data = data || {}; - event = $.Event( event ); - event.type = ( type === this.widgetEventPrefix ? - type : - this.widgetEventPrefix + type ).toLowerCase(); - // the original event may come from any element - // so we need to reset the target on the new event - event.target = this.element[ 0 ]; - - // copy original event properties over to the new event - orig = event.originalEvent; - if ( orig ) { - for ( prop in orig ) { - if ( !( prop in event ) ) { - event[ prop ] = orig[ prop ]; - } - } - } - - this.element.trigger( event, data ); - return !( $.isFunction( callback ) && - callback.apply( this.element[0], [ event ].concat( data ) ) === false || - event.isDefaultPrevented() ); - } - }; - - $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { - $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { - if ( typeof options === "string" ) { - options = { effect: options }; - } - var hasOptions, - effectName = !options ? - method : - options === true || typeof options === "number" ? - defaultEffect : - options.effect || defaultEffect; - options = options || {}; - if ( typeof options === "number" ) { - options = { duration: options }; - } - hasOptions = !$.isEmptyObject( options ); - options.complete = callback; - if ( options.delay ) { - element.delay( options.delay ); - } - if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { - element[ method ]( options ); - } else if ( effectName !== method && element[ effectName ] ) { - element[ effectName ]( options.duration, options.easing, callback ); - } else { - element.queue(function( next ) { - $( this )[ method ](); - if ( callback ) { - callback.call( element[ 0 ] ); - } - next(); - }); - } - }; - }); - - var widget = $.widget; - - - -})); diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_lunr.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_lunr.js deleted file mode 100644 index 31aa92464..000000000 --- a/samples/rest-notes-slate/slate/source/javascripts/lib/_lunr.js +++ /dev/null @@ -1,1910 +0,0 @@ -/** - * lunr - https://lunrjs.com - A bit like Solr, but much smaller and not as bright - 0.5.7 - * Copyright (C) 2014 Oliver Nightingale - * MIT Licensed - * @license - */ - -(function(){ - - /** - * Convenience function for instantiating a new lunr index and configuring it - * with the default pipeline functions and the passed config function. - * - * When using this convenience function a new index will be created with the - * following functions already in the pipeline: - * - * lunr.StopWordFilter - filters out any stop words before they enter the - * index - * - * lunr.stemmer - stems the tokens before entering the index. - * - * Example: - * - * var idx = lunr(function () { - * this.field('title', 10) - * this.field('tags', 100) - * this.field('body') - * - * this.ref('cid') - * - * this.pipeline.add(function () { - * // some custom pipeline function - * }) - * - * }) - * - * @param {Function} config A function that will be called with the new instance - * of the lunr.Index as both its context and first parameter. It can be used to - * customize the instance of new lunr.Index. - * @namespace - * @module - * @returns {lunr.Index} - * - */ - var lunr = function (config) { - var idx = new lunr.Index - - idx.pipeline.add( - lunr.trimmer, - lunr.stopWordFilter, - lunr.stemmer - ) - - if (config) config.call(idx, idx) - - return idx - } - - lunr.version = "0.5.7" - /*! - * lunr.utils - * Copyright (C) 2014 Oliver Nightingale - */ - - /** - * A namespace containing utils for the rest of the lunr library - */ - lunr.utils = {} - - /** - * Print a warning message to the console. - * - * @param {String} message The message to be printed. - * @memberOf Utils - */ - lunr.utils.warn = (function (global) { - return function (message) { - if (global.console && console.warn) { - console.warn(message) - } - } - })(this) - - /*! - * lunr.EventEmitter - * Copyright (C) 2014 Oliver Nightingale - */ - - /** - * lunr.EventEmitter is an event emitter for lunr. It manages adding and removing event handlers and triggering events and their handlers. - * - * @constructor - */ - lunr.EventEmitter = function () { - this.events = {} - } - - /** - * Binds a handler function to a specific event(s). - * - * Can bind a single function to many different events in one call. - * - * @param {String} [eventName] The name(s) of events to bind this function to. - * @param {Function} handler The function to call when an event is fired. - * @memberOf EventEmitter - */ - lunr.EventEmitter.prototype.addListener = function () { - var args = Array.prototype.slice.call(arguments), - fn = args.pop(), - names = args - - if (typeof fn !== "function") throw new TypeError ("last argument must be a function") - - names.forEach(function (name) { - if (!this.hasHandler(name)) this.events[name] = [] - this.events[name].push(fn) - }, this) - } - - /** - * Removes a handler function from a specific event. - * - * @param {String} eventName The name of the event to remove this function from. - * @param {Function} handler The function to remove from an event. - * @memberOf EventEmitter - */ - lunr.EventEmitter.prototype.removeListener = function (name, fn) { - if (!this.hasHandler(name)) return - - var fnIndex = this.events[name].indexOf(fn) - this.events[name].splice(fnIndex, 1) - - if (!this.events[name].length) delete this.events[name] - } - - /** - * Calls all functions bound to the given event. - * - * Additional data can be passed to the event handler as arguments to `emit` - * after the event name. - * - * @param {String} eventName The name of the event to emit. - * @memberOf EventEmitter - */ - lunr.EventEmitter.prototype.emit = function (name) { - if (!this.hasHandler(name)) return - - var args = Array.prototype.slice.call(arguments, 1) - - this.events[name].forEach(function (fn) { - fn.apply(undefined, args) - }) - } - - /** - * Checks whether a handler has ever been stored against an event. - * - * @param {String} eventName The name of the event to check. - * @private - * @memberOf EventEmitter - */ - lunr.EventEmitter.prototype.hasHandler = function (name) { - return name in this.events - } - - /*! - * lunr.tokenizer - * Copyright (C) 2014 Oliver Nightingale - */ - - /** - * A function for splitting a string into tokens ready to be inserted into - * the search index. - * - * @module - * @param {String} obj The string to convert into tokens - * @returns {Array} - */ - lunr.tokenizer = function (obj) { - if (!arguments.length || obj == null || obj == undefined) return [] - if (Array.isArray(obj)) return obj.map(function (t) { return t.toLowerCase() }) - - var str = obj.toString().replace(/^\s+/, '') - - for (var i = str.length - 1; i >= 0; i--) { - if (/\S/.test(str.charAt(i))) { - str = str.substring(0, i + 1) - break - } - } - - return str - .split(/(?:\s+|\-)/) - .filter(function (token) { - return !!token - }) - .map(function (token) { - return token.toLowerCase() - }) - } - /*! - * lunr.Pipeline - * Copyright (C) 2014 Oliver Nightingale - */ - - /** - * lunr.Pipelines maintain an ordered list of functions to be applied to all - * tokens in documents entering the search index and queries being ran against - * the index. - * - * An instance of lunr.Index created with the lunr shortcut will contain a - * pipeline with a stop word filter and an English language stemmer. Extra - * functions can be added before or after either of these functions or these - * default functions can be removed. - * - * When run the pipeline will call each function in turn, passing a token, the - * index of that token in the original list of all tokens and finally a list of - * all the original tokens. - * - * The output of functions in the pipeline will be passed to the next function - * in the pipeline. To exclude a token from entering the index the function - * should return undefined, the rest of the pipeline will not be called with - * this token. - * - * For serialisation of pipelines to work, all functions used in an instance of - * a pipeline should be registered with lunr.Pipeline. Registered functions can - * then be loaded. If trying to load a serialised pipeline that uses functions - * that are not registered an error will be thrown. - * - * If not planning on serialising the pipeline then registering pipeline functions - * is not necessary. - * - * @constructor - */ - lunr.Pipeline = function () { - this._stack = [] - } - - lunr.Pipeline.registeredFunctions = {} - - /** - * Register a function with the pipeline. - * - * Functions that are used in the pipeline should be registered if the pipeline - * needs to be serialised, or a serialised pipeline needs to be loaded. - * - * Registering a function does not add it to a pipeline, functions must still be - * added to instances of the pipeline for them to be used when running a pipeline. - * - * @param {Function} fn The function to check for. - * @param {String} label The label to register this function with - * @memberOf Pipeline - */ - lunr.Pipeline.registerFunction = function (fn, label) { - if (label in this.registeredFunctions) { - lunr.utils.warn('Overwriting existing registered function: ' + label) - } - - fn.label = label - lunr.Pipeline.registeredFunctions[fn.label] = fn - } - - /** - * Warns if the function is not registered as a Pipeline function. - * - * @param {Function} fn The function to check for. - * @private - * @memberOf Pipeline - */ - lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { - var isRegistered = fn.label && (fn.label in this.registeredFunctions) - - if (!isRegistered) { - lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) - } - } - - /** - * Loads a previously serialised pipeline. - * - * All functions to be loaded must already be registered with lunr.Pipeline. - * If any function from the serialised data has not been registered then an - * error will be thrown. - * - * @param {Object} serialised The serialised pipeline to load. - * @returns {lunr.Pipeline} - * @memberOf Pipeline - */ - lunr.Pipeline.load = function (serialised) { - var pipeline = new lunr.Pipeline - - serialised.forEach(function (fnName) { - var fn = lunr.Pipeline.registeredFunctions[fnName] - - if (fn) { - pipeline.add(fn) - } else { - throw new Error ('Cannot load un-registered function: ' + fnName) - } - }) - - return pipeline - } - - /** - * Adds new functions to the end of the pipeline. - * - * Logs a warning if the function has not been registered. - * - * @param {Function} functions Any number of functions to add to the pipeline. - * @memberOf Pipeline - */ - lunr.Pipeline.prototype.add = function () { - var fns = Array.prototype.slice.call(arguments) - - fns.forEach(function (fn) { - lunr.Pipeline.warnIfFunctionNotRegistered(fn) - this._stack.push(fn) - }, this) - } - - /** - * Adds a single function after a function that already exists in the - * pipeline. - * - * Logs a warning if the function has not been registered. - * - * @param {Function} existingFn A function that already exists in the pipeline. - * @param {Function} newFn The new function to add to the pipeline. - * @memberOf Pipeline - */ - lunr.Pipeline.prototype.after = function (existingFn, newFn) { - lunr.Pipeline.warnIfFunctionNotRegistered(newFn) - - var pos = this._stack.indexOf(existingFn) + 1 - this._stack.splice(pos, 0, newFn) - } - - /** - * Adds a single function before a function that already exists in the - * pipeline. - * - * Logs a warning if the function has not been registered. - * - * @param {Function} existingFn A function that already exists in the pipeline. - * @param {Function} newFn The new function to add to the pipeline. - * @memberOf Pipeline - */ - lunr.Pipeline.prototype.before = function (existingFn, newFn) { - lunr.Pipeline.warnIfFunctionNotRegistered(newFn) - - var pos = this._stack.indexOf(existingFn) - this._stack.splice(pos, 0, newFn) - } - - /** - * Removes a function from the pipeline. - * - * @param {Function} fn The function to remove from the pipeline. - * @memberOf Pipeline - */ - lunr.Pipeline.prototype.remove = function (fn) { - var pos = this._stack.indexOf(fn) - this._stack.splice(pos, 1) - } - - /** - * Runs the current list of functions that make up the pipeline against the - * passed tokens. - * - * @param {Array} tokens The tokens to run through the pipeline. - * @returns {Array} - * @memberOf Pipeline - */ - lunr.Pipeline.prototype.run = function (tokens) { - var out = [], - tokenLength = tokens.length, - stackLength = this._stack.length - - for (var i = 0; i < tokenLength; i++) { - var token = tokens[i] - - for (var j = 0; j < stackLength; j++) { - token = this._stack[j](token, i, tokens) - if (token === void 0) break - }; - - if (token !== void 0) out.push(token) - }; - - return out - } - - /** - * Resets the pipeline by removing any existing processors. - * - * @memberOf Pipeline - */ - lunr.Pipeline.prototype.reset = function () { - this._stack = [] - } - - /** - * Returns a representation of the pipeline ready for serialisation. - * - * Logs a warning if the function has not been registered. - * - * @returns {Array} - * @memberOf Pipeline - */ - lunr.Pipeline.prototype.toJSON = function () { - return this._stack.map(function (fn) { - lunr.Pipeline.warnIfFunctionNotRegistered(fn) - - return fn.label - }) - } - /*! - * lunr.Vector - * Copyright (C) 2014 Oliver Nightingale - */ - - /** - * lunr.Vectors implement vector related operations for - * a series of elements. - * - * @constructor - */ - lunr.Vector = function () { - this._magnitude = null - this.list = undefined - this.length = 0 - } - - /** - * lunr.Vector.Node is a simple struct for each node - * in a lunr.Vector. - * - * @private - * @param {Number} The index of the node in the vector. - * @param {Object} The data at this node in the vector. - * @param {lunr.Vector.Node} The node directly after this node in the vector. - * @constructor - * @memberOf Vector - */ - lunr.Vector.Node = function (idx, val, next) { - this.idx = idx - this.val = val - this.next = next - } - - /** - * Inserts a new value at a position in a vector. - * - * @param {Number} The index at which to insert a value. - * @param {Object} The object to insert in the vector. - * @memberOf Vector. - */ - lunr.Vector.prototype.insert = function (idx, val) { - var list = this.list - - if (!list) { - this.list = new lunr.Vector.Node (idx, val, list) - return this.length++ - } - - var prev = list, - next = list.next - - while (next != undefined) { - if (idx < next.idx) { - prev.next = new lunr.Vector.Node (idx, val, next) - return this.length++ - } - - prev = next, next = next.next - } - - prev.next = new lunr.Vector.Node (idx, val, next) - return this.length++ - } - - /** - * Calculates the magnitude of this vector. - * - * @returns {Number} - * @memberOf Vector - */ - lunr.Vector.prototype.magnitude = function () { - if (this._magniture) return this._magnitude - var node = this.list, - sumOfSquares = 0, - val - - while (node) { - val = node.val - sumOfSquares += val * val - node = node.next - } - - return this._magnitude = Math.sqrt(sumOfSquares) - } - - /** - * Calculates the dot product of this vector and another vector. - * - * @param {lunr.Vector} otherVector The vector to compute the dot product with. - * @returns {Number} - * @memberOf Vector - */ - lunr.Vector.prototype.dot = function (otherVector) { - var node = this.list, - otherNode = otherVector.list, - dotProduct = 0 - - while (node && otherNode) { - if (node.idx < otherNode.idx) { - node = node.next - } else if (node.idx > otherNode.idx) { - otherNode = otherNode.next - } else { - dotProduct += node.val * otherNode.val - node = node.next - otherNode = otherNode.next - } - } - - return dotProduct - } - - /** - * Calculates the cosine similarity between this vector and another - * vector. - * - * @param {lunr.Vector} otherVector The other vector to calculate the - * similarity with. - * @returns {Number} - * @memberOf Vector - */ - lunr.Vector.prototype.similarity = function (otherVector) { - return this.dot(otherVector) / (this.magnitude() * otherVector.magnitude()) - } - /*! - * lunr.SortedSet - * Copyright (C) 2014 Oliver Nightingale - */ - - /** - * lunr.SortedSets are used to maintain an array of uniq values in a sorted - * order. - * - * @constructor - */ - lunr.SortedSet = function () { - this.length = 0 - this.elements = [] - } - - /** - * Loads a previously serialised sorted set. - * - * @param {Array} serialisedData The serialised set to load. - * @returns {lunr.SortedSet} - * @memberOf SortedSet - */ - lunr.SortedSet.load = function (serialisedData) { - var set = new this - - set.elements = serialisedData - set.length = serialisedData.length - - return set - } - - /** - * Inserts new items into the set in the correct position to maintain the - * order. - * - * @param {Object} The objects to add to this set. - * @memberOf SortedSet - */ - lunr.SortedSet.prototype.add = function () { - Array.prototype.slice.call(arguments).forEach(function (element) { - if (~this.indexOf(element)) return - this.elements.splice(this.locationFor(element), 0, element) - }, this) - - this.length = this.elements.length - } - - /** - * Converts this sorted set into an array. - * - * @returns {Array} - * @memberOf SortedSet - */ - lunr.SortedSet.prototype.toArray = function () { - return this.elements.slice() - } - - /** - * Creates a new array with the results of calling a provided function on every - * element in this sorted set. - * - * Delegates to Array.prototype.map and has the same signature. - * - * @param {Function} fn The function that is called on each element of the - * set. - * @param {Object} ctx An optional object that can be used as the context - * for the function fn. - * @returns {Array} - * @memberOf SortedSet - */ - lunr.SortedSet.prototype.map = function (fn, ctx) { - return this.elements.map(fn, ctx) - } - - /** - * Executes a provided function once per sorted set element. - * - * Delegates to Array.prototype.forEach and has the same signature. - * - * @param {Function} fn The function that is called on each element of the - * set. - * @param {Object} ctx An optional object that can be used as the context - * @memberOf SortedSet - * for the function fn. - */ - lunr.SortedSet.prototype.forEach = function (fn, ctx) { - return this.elements.forEach(fn, ctx) - } - - /** - * Returns the index at which a given element can be found in the - * sorted set, or -1 if it is not present. - * - * @param {Object} elem The object to locate in the sorted set. - * @param {Number} start An optional index at which to start searching from - * within the set. - * @param {Number} end An optional index at which to stop search from within - * the set. - * @returns {Number} - * @memberOf SortedSet - */ - lunr.SortedSet.prototype.indexOf = function (elem, start, end) { - var start = start || 0, - end = end || this.elements.length, - sectionLength = end - start, - pivot = start + Math.floor(sectionLength / 2), - pivotElem = this.elements[pivot] - - if (sectionLength <= 1) { - if (pivotElem === elem) { - return pivot - } else { - return -1 - } - } - - if (pivotElem < elem) return this.indexOf(elem, pivot, end) - if (pivotElem > elem) return this.indexOf(elem, start, pivot) - if (pivotElem === elem) return pivot - } - - /** - * Returns the position within the sorted set that an element should be - * inserted at to maintain the current order of the set. - * - * This function assumes that the element to search for does not already exist - * in the sorted set. - * - * @param {Object} elem The elem to find the position for in the set - * @param {Number} start An optional index at which to start searching from - * within the set. - * @param {Number} end An optional index at which to stop search from within - * the set. - * @returns {Number} - * @memberOf SortedSet - */ - lunr.SortedSet.prototype.locationFor = function (elem, start, end) { - var start = start || 0, - end = end || this.elements.length, - sectionLength = end - start, - pivot = start + Math.floor(sectionLength / 2), - pivotElem = this.elements[pivot] - - if (sectionLength <= 1) { - if (pivotElem > elem) return pivot - if (pivotElem < elem) return pivot + 1 - } - - if (pivotElem < elem) return this.locationFor(elem, pivot, end) - if (pivotElem > elem) return this.locationFor(elem, start, pivot) - } - - /** - * Creates a new lunr.SortedSet that contains the elements in the intersection - * of this set and the passed set. - * - * @param {lunr.SortedSet} otherSet The set to intersect with this set. - * @returns {lunr.SortedSet} - * @memberOf SortedSet - */ - lunr.SortedSet.prototype.intersect = function (otherSet) { - var intersectSet = new lunr.SortedSet, - i = 0, j = 0, - a_len = this.length, b_len = otherSet.length, - a = this.elements, b = otherSet.elements - - while (true) { - if (i > a_len - 1 || j > b_len - 1) break - - if (a[i] === b[j]) { - intersectSet.add(a[i]) - i++, j++ - continue - } - - if (a[i] < b[j]) { - i++ - continue - } - - if (a[i] > b[j]) { - j++ - continue - } - }; - - return intersectSet - } - - /** - * Makes a copy of this set - * - * @returns {lunr.SortedSet} - * @memberOf SortedSet - */ - lunr.SortedSet.prototype.clone = function () { - var clone = new lunr.SortedSet - - clone.elements = this.toArray() - clone.length = clone.elements.length - - return clone - } - - /** - * Creates a new lunr.SortedSet that contains the elements in the union - * of this set and the passed set. - * - * @param {lunr.SortedSet} otherSet The set to union with this set. - * @returns {lunr.SortedSet} - * @memberOf SortedSet - */ - lunr.SortedSet.prototype.union = function (otherSet) { - var longSet, shortSet, unionSet - - if (this.length >= otherSet.length) { - longSet = this, shortSet = otherSet - } else { - longSet = otherSet, shortSet = this - } - - unionSet = longSet.clone() - - unionSet.add.apply(unionSet, shortSet.toArray()) - - return unionSet - } - - /** - * Returns a representation of the sorted set ready for serialisation. - * - * @returns {Array} - * @memberOf SortedSet - */ - lunr.SortedSet.prototype.toJSON = function () { - return this.toArray() - } - /*! - * lunr.Index - * Copyright (C) 2014 Oliver Nightingale - */ - - /** - * lunr.Index is object that manages a search index. It contains the indexes - * and stores all the tokens and document lookups. It also provides the main - * user facing API for the library. - * - * @constructor - */ - lunr.Index = function () { - this._fields = [] - this._ref = 'id' - this.pipeline = new lunr.Pipeline - this.documentStore = new lunr.Store - this.tokenStore = new lunr.TokenStore - this.corpusTokens = new lunr.SortedSet - this.eventEmitter = new lunr.EventEmitter - - this._idfCache = {} - - this.on('add', 'remove', 'update', (function () { - this._idfCache = {} - }).bind(this)) - } - - /** - * Bind a handler to events being emitted by the index. - * - * The handler can be bound to many events at the same time. - * - * @param {String} [eventName] The name(s) of events to bind the function to. - * @param {Function} handler The serialised set to load. - * @memberOf Index - */ - lunr.Index.prototype.on = function () { - var args = Array.prototype.slice.call(arguments) - return this.eventEmitter.addListener.apply(this.eventEmitter, args) - } - - /** - * Removes a handler from an event being emitted by the index. - * - * @param {String} eventName The name of events to remove the function from. - * @param {Function} handler The serialised set to load. - * @memberOf Index - */ - lunr.Index.prototype.off = function (name, fn) { - return this.eventEmitter.removeListener(name, fn) - } - - /** - * Loads a previously serialised index. - * - * Issues a warning if the index being imported was serialised - * by a different version of lunr. - * - * @param {Object} serialisedData The serialised set to load. - * @returns {lunr.Index} - * @memberOf Index - */ - lunr.Index.load = function (serialisedData) { - if (serialisedData.version !== lunr.version) { - lunr.utils.warn('version mismatch: current ' + lunr.version + ' importing ' + serialisedData.version) - } - - var idx = new this - - idx._fields = serialisedData.fields - idx._ref = serialisedData.ref - - idx.documentStore = lunr.Store.load(serialisedData.documentStore) - idx.tokenStore = lunr.TokenStore.load(serialisedData.tokenStore) - idx.corpusTokens = lunr.SortedSet.load(serialisedData.corpusTokens) - idx.pipeline = lunr.Pipeline.load(serialisedData.pipeline) - - return idx - } - - /** - * Adds a field to the list of fields that will be searchable within documents - * in the index. - * - * An optional boost param can be passed to affect how much tokens in this field - * rank in search results, by default the boost value is 1. - * - * Fields should be added before any documents are added to the index, fields - * that are added after documents are added to the index will only apply to new - * documents added to the index. - * - * @param {String} fieldName The name of the field within the document that - * should be indexed - * @param {Number} boost An optional boost that can be applied to terms in this - * field. - * @returns {lunr.Index} - * @memberOf Index - */ - lunr.Index.prototype.field = function (fieldName, opts) { - var opts = opts || {}, - field = { name: fieldName, boost: opts.boost || 1 } - - this._fields.push(field) - return this - } - - /** - * Sets the property used to uniquely identify documents added to the index, - * by default this property is 'id'. - * - * This should only be changed before adding documents to the index, changing - * the ref property without resetting the index can lead to unexpected results. - * - * @param {String} refName The property to use to uniquely identify the - * documents in the index. - * @param {Boolean} emitEvent Whether to emit add events, defaults to true - * @returns {lunr.Index} - * @memberOf Index - */ - lunr.Index.prototype.ref = function (refName) { - this._ref = refName - return this - } - - /** - * Add a document to the index. - * - * This is the way new documents enter the index, this function will run the - * fields from the document through the index's pipeline and then add it to - * the index, it will then show up in search results. - * - * An 'add' event is emitted with the document that has been added and the index - * the document has been added to. This event can be silenced by passing false - * as the second argument to add. - * - * @param {Object} doc The document to add to the index. - * @param {Boolean} emitEvent Whether or not to emit events, default true. - * @memberOf Index - */ - lunr.Index.prototype.add = function (doc, emitEvent) { - var docTokens = {}, - allDocumentTokens = new lunr.SortedSet, - docRef = doc[this._ref], - emitEvent = emitEvent === undefined ? true : emitEvent - - this._fields.forEach(function (field) { - var fieldTokens = this.pipeline.run(lunr.tokenizer(doc[field.name])) - - docTokens[field.name] = fieldTokens - lunr.SortedSet.prototype.add.apply(allDocumentTokens, fieldTokens) - }, this) - - this.documentStore.set(docRef, allDocumentTokens) - lunr.SortedSet.prototype.add.apply(this.corpusTokens, allDocumentTokens.toArray()) - - for (var i = 0; i < allDocumentTokens.length; i++) { - var token = allDocumentTokens.elements[i] - var tf = this._fields.reduce(function (memo, field) { - var fieldLength = docTokens[field.name].length - - if (!fieldLength) return memo - - var tokenCount = docTokens[field.name].filter(function (t) { return t === token }).length - - return memo + (tokenCount / fieldLength * field.boost) - }, 0) - - this.tokenStore.add(token, { ref: docRef, tf: tf }) - }; - - if (emitEvent) this.eventEmitter.emit('add', doc, this) - } - - /** - * Removes a document from the index. - * - * To make sure documents no longer show up in search results they can be - * removed from the index using this method. - * - * The document passed only needs to have the same ref property value as the - * document that was added to the index, they could be completely different - * objects. - * - * A 'remove' event is emitted with the document that has been removed and the index - * the document has been removed from. This event can be silenced by passing false - * as the second argument to remove. - * - * @param {Object} doc The document to remove from the index. - * @param {Boolean} emitEvent Whether to emit remove events, defaults to true - * @memberOf Index - */ - lunr.Index.prototype.remove = function (doc, emitEvent) { - var docRef = doc[this._ref], - emitEvent = emitEvent === undefined ? true : emitEvent - - if (!this.documentStore.has(docRef)) return - - var docTokens = this.documentStore.get(docRef) - - this.documentStore.remove(docRef) - - docTokens.forEach(function (token) { - this.tokenStore.remove(token, docRef) - }, this) - - if (emitEvent) this.eventEmitter.emit('remove', doc, this) - } - - /** - * Updates a document in the index. - * - * When a document contained within the index gets updated, fields changed, - * added or removed, to make sure it correctly matched against search queries, - * it should be updated in the index. - * - * This method is just a wrapper around `remove` and `add` - * - * An 'update' event is emitted with the document that has been updated and the index. - * This event can be silenced by passing false as the second argument to update. Only - * an update event will be fired, the 'add' and 'remove' events of the underlying calls - * are silenced. - * - * @param {Object} doc The document to update in the index. - * @param {Boolean} emitEvent Whether to emit update events, defaults to true - * @see Index.prototype.remove - * @see Index.prototype.add - * @memberOf Index - */ - lunr.Index.prototype.update = function (doc, emitEvent) { - var emitEvent = emitEvent === undefined ? true : emitEvent - - this.remove(doc, false) - this.add(doc, false) - - if (emitEvent) this.eventEmitter.emit('update', doc, this) - } - - /** - * Calculates the inverse document frequency for a token within the index. - * - * @param {String} token The token to calculate the idf of. - * @see Index.prototype.idf - * @private - * @memberOf Index - */ - lunr.Index.prototype.idf = function (term) { - var cacheKey = "@" + term - if (Object.prototype.hasOwnProperty.call(this._idfCache, cacheKey)) return this._idfCache[cacheKey] - - var documentFrequency = this.tokenStore.count(term), - idf = 1 - - if (documentFrequency > 0) { - idf = 1 + Math.log(this.tokenStore.length / documentFrequency) - } - - return this._idfCache[cacheKey] = idf - } - - /** - * Searches the index using the passed query. - * - * Queries should be a string, multiple words are allowed and will lead to an - * AND based query, e.g. `idx.search('foo bar')` will run a search for - * documents containing both 'foo' and 'bar'. - * - * All query tokens are passed through the same pipeline that document tokens - * are passed through, so any language processing involved will be run on every - * query term. - * - * Each query term is expanded, so that the term 'he' might be expanded to - * 'hello' and 'help' if those terms were already included in the index. - * - * Matching documents are returned as an array of objects, each object contains - * the matching document ref, as set for this index, and the similarity score - * for this document against the query. - * - * @param {String} query The query to search the index with. - * @returns {Object} - * @see Index.prototype.idf - * @see Index.prototype.documentVector - * @memberOf Index - */ - lunr.Index.prototype.search = function (query) { - var queryTokens = this.pipeline.run(lunr.tokenizer(query)), - queryVector = new lunr.Vector, - documentSets = [], - fieldBoosts = this._fields.reduce(function (memo, f) { return memo + f.boost }, 0) - - var hasSomeToken = queryTokens.some(function (token) { - return this.tokenStore.has(token) - }, this) - - if (!hasSomeToken) return [] - - queryTokens - .forEach(function (token, i, tokens) { - var tf = 1 / tokens.length * this._fields.length * fieldBoosts, - self = this - - var set = this.tokenStore.expand(token).reduce(function (memo, key) { - var pos = self.corpusTokens.indexOf(key), - idf = self.idf(key), - similarityBoost = 1, - set = new lunr.SortedSet - - // if the expanded key is not an exact match to the token then - // penalise the score for this key by how different the key is - // to the token. - if (key !== token) { - var diff = Math.max(3, key.length - token.length) - similarityBoost = 1 / Math.log(diff) - } - - // calculate the query tf-idf score for this token - // applying an similarityBoost to ensure exact matches - // these rank higher than expanded terms - if (pos > -1) queryVector.insert(pos, tf * idf * similarityBoost) - - // add all the documents that have this key into a set - Object.keys(self.tokenStore.get(key)).forEach(function (ref) { set.add(ref) }) - - return memo.union(set) - }, new lunr.SortedSet) - - documentSets.push(set) - }, this) - - var documentSet = documentSets.reduce(function (memo, set) { - return memo.intersect(set) - }) - - return documentSet - .map(function (ref) { - return { ref: ref, score: queryVector.similarity(this.documentVector(ref)) } - }, this) - .sort(function (a, b) { - return b.score - a.score - }) - } - - /** - * Generates a vector containing all the tokens in the document matching the - * passed documentRef. - * - * The vector contains the tf-idf score for each token contained in the - * document with the passed documentRef. The vector will contain an element - * for every token in the indexes corpus, if the document does not contain that - * token the element will be 0. - * - * @param {Object} documentRef The ref to find the document with. - * @returns {lunr.Vector} - * @private - * @memberOf Index - */ - lunr.Index.prototype.documentVector = function (documentRef) { - var documentTokens = this.documentStore.get(documentRef), - documentTokensLength = documentTokens.length, - documentVector = new lunr.Vector - - for (var i = 0; i < documentTokensLength; i++) { - var token = documentTokens.elements[i], - tf = this.tokenStore.get(token)[documentRef].tf, - idf = this.idf(token) - - documentVector.insert(this.corpusTokens.indexOf(token), tf * idf) - }; - - return documentVector - } - - /** - * Returns a representation of the index ready for serialisation. - * - * @returns {Object} - * @memberOf Index - */ - lunr.Index.prototype.toJSON = function () { - return { - version: lunr.version, - fields: this._fields, - ref: this._ref, - documentStore: this.documentStore.toJSON(), - tokenStore: this.tokenStore.toJSON(), - corpusTokens: this.corpusTokens.toJSON(), - pipeline: this.pipeline.toJSON() - } - } - - /** - * Applies a plugin to the current index. - * - * A plugin is a function that is called with the index as its context. - * Plugins can be used to customise or extend the behaviour the index - * in some way. A plugin is just a function, that encapsulated the custom - * behaviour that should be applied to the index. - * - * The plugin function will be called with the index as its argument, additional - * arguments can also be passed when calling use. The function will be called - * with the index as its context. - * - * Example: - * - * var myPlugin = function (idx, arg1, arg2) { - * // `this` is the index to be extended - * // apply any extensions etc here. - * } - * - * var idx = lunr(function () { - * this.use(myPlugin, 'arg1', 'arg2') - * }) - * - * @param {Function} plugin The plugin to apply. - * @memberOf Index - */ - lunr.Index.prototype.use = function (plugin) { - var args = Array.prototype.slice.call(arguments, 1) - args.unshift(this) - plugin.apply(this, args) - } - /*! - * lunr.Store - * Copyright (C) 2014 Oliver Nightingale - */ - - /** - * lunr.Store is a simple key-value store used for storing sets of tokens for - * documents stored in index. - * - * @constructor - * @module - */ - lunr.Store = function () { - this.store = {} - this.length = 0 - } - - /** - * Loads a previously serialised store - * - * @param {Object} serialisedData The serialised store to load. - * @returns {lunr.Store} - * @memberOf Store - */ - lunr.Store.load = function (serialisedData) { - var store = new this - - store.length = serialisedData.length - store.store = Object.keys(serialisedData.store).reduce(function (memo, key) { - memo[key] = lunr.SortedSet.load(serialisedData.store[key]) - return memo - }, {}) - - return store - } - - /** - * Stores the given tokens in the store against the given id. - * - * @param {Object} id The key used to store the tokens against. - * @param {Object} tokens The tokens to store against the key. - * @memberOf Store - */ - lunr.Store.prototype.set = function (id, tokens) { - if (!this.has(id)) this.length++ - this.store[id] = tokens - } - - /** - * Retrieves the tokens from the store for a given key. - * - * @param {Object} id The key to lookup and retrieve from the store. - * @returns {Object} - * @memberOf Store - */ - lunr.Store.prototype.get = function (id) { - return this.store[id] - } - - /** - * Checks whether the store contains a key. - * - * @param {Object} id The id to look up in the store. - * @returns {Boolean} - * @memberOf Store - */ - lunr.Store.prototype.has = function (id) { - return id in this.store - } - - /** - * Removes the value for a key in the store. - * - * @param {Object} id The id to remove from the store. - * @memberOf Store - */ - lunr.Store.prototype.remove = function (id) { - if (!this.has(id)) return - - delete this.store[id] - this.length-- - } - - /** - * Returns a representation of the store ready for serialisation. - * - * @returns {Object} - * @memberOf Store - */ - lunr.Store.prototype.toJSON = function () { - return { - store: this.store, - length: this.length - } - } - - /*! - * lunr.stemmer - * Copyright (C) 2014 Oliver Nightingale - * Includes code from - https://tartarus.org/~martin/PorterStemmer/js.txt - */ - - /** - * lunr.stemmer is an english language stemmer, this is a JavaScript - * implementation of the PorterStemmer taken from https://tartaurs.org/~martin - * - * @module - * @param {String} str The string to stem - * @returns {String} - * @see lunr.Pipeline - */ - lunr.stemmer = (function(){ - var step2list = { - "ational" : "ate", - "tional" : "tion", - "enci" : "ence", - "anci" : "ance", - "izer" : "ize", - "bli" : "ble", - "alli" : "al", - "entli" : "ent", - "eli" : "e", - "ousli" : "ous", - "ization" : "ize", - "ation" : "ate", - "ator" : "ate", - "alism" : "al", - "iveness" : "ive", - "fulness" : "ful", - "ousness" : "ous", - "aliti" : "al", - "iviti" : "ive", - "biliti" : "ble", - "logi" : "log" - }, - - step3list = { - "icate" : "ic", - "ative" : "", - "alize" : "al", - "iciti" : "ic", - "ical" : "ic", - "ful" : "", - "ness" : "" - }, - - c = "[^aeiou]", // consonant - v = "[aeiouy]", // vowel - C = c + "[^aeiouy]*", // consonant sequence - V = v + "[aeiou]*", // vowel sequence - - mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 - meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 - mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 - s_v = "^(" + C + ")?" + v; // vowel in stem - - var re_mgr0 = new RegExp(mgr0); - var re_mgr1 = new RegExp(mgr1); - var re_meq1 = new RegExp(meq1); - var re_s_v = new RegExp(s_v); - - var re_1a = /^(.+?)(ss|i)es$/; - var re2_1a = /^(.+?)([^s])s$/; - var re_1b = /^(.+?)eed$/; - var re2_1b = /^(.+?)(ed|ing)$/; - var re_1b_2 = /.$/; - var re2_1b_2 = /(at|bl|iz)$/; - var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$"); - var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - - var re_1c = /^(.+?[^aeiou])y$/; - var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; - - var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; - - var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; - var re2_4 = /^(.+?)(s|t)(ion)$/; - - var re_5 = /^(.+?)e$/; - var re_5_1 = /ll$/; - var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - - var porterStemmer = function porterStemmer(w) { - var stem, - suffix, - firstch, - re, - re2, - re3, - re4; - - if (w.length < 3) { return w; } - - firstch = w.substr(0,1); - if (firstch == "y") { - w = firstch.toUpperCase() + w.substr(1); - } - - // Step 1a - re = re_1a - re2 = re2_1a; - - if (re.test(w)) { w = w.replace(re,"$1$2"); } - else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } - - // Step 1b - re = re_1b; - re2 = re2_1b; - if (re.test(w)) { - var fp = re.exec(w); - re = re_mgr0; - if (re.test(fp[1])) { - re = re_1b_2; - w = w.replace(re,""); - } - } else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1]; - re2 = re_s_v; - if (re2.test(stem)) { - w = stem; - re2 = re2_1b_2; - re3 = re3_1b_2; - re4 = re4_1b_2; - if (re2.test(w)) { w = w + "e"; } - else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); } - else if (re4.test(w)) { w = w + "e"; } - } - } - - // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say) - re = re_1c; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - w = stem + "i"; - } - - // Step 2 - re = re_2; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = re_mgr0; - if (re.test(stem)) { - w = stem + step2list[suffix]; - } - } - - // Step 3 - re = re_3; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = re_mgr0; - if (re.test(stem)) { - w = stem + step3list[suffix]; - } - } - - // Step 4 - re = re_4; - re2 = re2_4; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = re_mgr1; - if (re.test(stem)) { - w = stem; - } - } else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1] + fp[2]; - re2 = re_mgr1; - if (re2.test(stem)) { - w = stem; - } - } - - // Step 5 - re = re_5; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = re_mgr1; - re2 = re_meq1; - re3 = re3_5; - if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { - w = stem; - } - } - - re = re_5_1; - re2 = re_mgr1; - if (re.test(w) && re2.test(w)) { - re = re_1b_2; - w = w.replace(re,""); - } - - // and turn initial Y back to y - - if (firstch == "y") { - w = firstch.toLowerCase() + w.substr(1); - } - - return w; - }; - - return porterStemmer; - })(); - - lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') - /*! - * lunr.stopWordFilter - * Copyright (C) 2014 Oliver Nightingale - */ - - /** - * lunr.stopWordFilter is an English language stop word list filter, any words - * contained in the list will not be passed through the filter. - * - * This is intended to be used in the Pipeline. If the token does not pass the - * filter then undefined will be returned. - * - * @module - * @param {String} token The token to pass through the filter - * @returns {String} - * @see lunr.Pipeline - */ - lunr.stopWordFilter = function (token) { - if (lunr.stopWordFilter.stopWords.indexOf(token) === -1) return token - } - - lunr.stopWordFilter.stopWords = new lunr.SortedSet - lunr.stopWordFilter.stopWords.length = 119 - lunr.stopWordFilter.stopWords.elements = [ - "", - "a", - "able", - "about", - "across", - "after", - "all", - "almost", - "also", - "am", - "among", - "an", - "and", - "any", - "are", - "as", - "at", - "be", - "because", - "been", - "but", - "by", - "can", - "cannot", - "could", - "dear", - "did", - "do", - "does", - "either", - "else", - "ever", - "every", - "for", - "from", - "get", - "got", - "had", - "has", - "have", - "he", - "her", - "hers", - "him", - "his", - "how", - "however", - "i", - "if", - "in", - "into", - "is", - "it", - "its", - "just", - "least", - "let", - "like", - "likely", - "may", - "me", - "might", - "most", - "must", - "my", - "neither", - "no", - "nor", - "not", - "of", - "off", - "often", - "on", - "only", - "or", - "other", - "our", - "own", - "rather", - "said", - "say", - "says", - "she", - "should", - "since", - "so", - "some", - "than", - "that", - "the", - "their", - "them", - "then", - "there", - "these", - "they", - "this", - "tis", - "to", - "too", - "twas", - "us", - "wants", - "was", - "we", - "were", - "what", - "when", - "where", - "which", - "while", - "who", - "whom", - "why", - "will", - "with", - "would", - "yet", - "you", - "your" - ] - - lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') - /*! - * lunr.trimmer - * Copyright (C) 2014 Oliver Nightingale - */ - - /** - * lunr.trimmer is a pipeline function for trimming non word - * characters from the begining and end of tokens before they - * enter the index. - * - * This implementation may not work correctly for non latin - * characters and should either be removed or adapted for use - * with languages with non-latin characters. - * - * @module - * @param {String} token The token to pass through the filter - * @returns {String} - * @see lunr.Pipeline - */ - lunr.trimmer = function (token) { - return token - .replace(/^\W+/, '') - .replace(/\W+$/, '') - } - - lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') - /*! - * lunr.stemmer - * Copyright (C) 2014 Oliver Nightingale - * Includes code from - https://tartarus.org/~martin/PorterStemmer/js.txt - */ - - /** - * lunr.TokenStore is used for efficient storing and lookup of the reverse - * index of token to document ref. - * - * @constructor - */ - lunr.TokenStore = function () { - this.root = { docs: {} } - this.length = 0 - } - - /** - * Loads a previously serialised token store - * - * @param {Object} serialisedData The serialised token store to load. - * @returns {lunr.TokenStore} - * @memberOf TokenStore - */ - lunr.TokenStore.load = function (serialisedData) { - var store = new this - - store.root = serialisedData.root - store.length = serialisedData.length - - return store - } - - /** - * Adds a new token doc pair to the store. - * - * By default this function starts at the root of the current store, however - * it can start at any node of any token store if required. - * - * @param {String} token The token to store the doc under - * @param {Object} doc The doc to store against the token - * @param {Object} root An optional node at which to start looking for the - * correct place to enter the doc, by default the root of this lunr.TokenStore - * is used. - * @memberOf TokenStore - */ - lunr.TokenStore.prototype.add = function (token, doc, root) { - var root = root || this.root, - key = token[0], - rest = token.slice(1) - - if (!(key in root)) root[key] = {docs: {}} - - if (rest.length === 0) { - root[key].docs[doc.ref] = doc - this.length += 1 - return - } else { - return this.add(rest, doc, root[key]) - } - } - - /** - * Checks whether this key is contained within this lunr.TokenStore. - * - * By default this function starts at the root of the current store, however - * it can start at any node of any token store if required. - * - * @param {String} token The token to check for - * @param {Object} root An optional node at which to start - * @memberOf TokenStore - */ - lunr.TokenStore.prototype.has = function (token) { - if (!token) return false - - var node = this.root - - for (var i = 0; i < token.length; i++) { - if (!node[token[i]]) return false - - node = node[token[i]] - } - - return true - } - - /** - * Retrieve a node from the token store for a given token. - * - * By default this function starts at the root of the current store, however - * it can start at any node of any token store if required. - * - * @param {String} token The token to get the node for. - * @param {Object} root An optional node at which to start. - * @returns {Object} - * @see TokenStore.prototype.get - * @memberOf TokenStore - */ - lunr.TokenStore.prototype.getNode = function (token) { - if (!token) return {} - - var node = this.root - - for (var i = 0; i < token.length; i++) { - if (!node[token[i]]) return {} - - node = node[token[i]] - } - - return node - } - - /** - * Retrieve the documents for a node for the given token. - * - * By default this function starts at the root of the current store, however - * it can start at any node of any token store if required. - * - * @param {String} token The token to get the documents for. - * @param {Object} root An optional node at which to start. - * @returns {Object} - * @memberOf TokenStore - */ - lunr.TokenStore.prototype.get = function (token, root) { - return this.getNode(token, root).docs || {} - } - - lunr.TokenStore.prototype.count = function (token, root) { - return Object.keys(this.get(token, root)).length - } - - /** - * Remove the document identified by ref from the token in the store. - * - * By default this function starts at the root of the current store, however - * it can start at any node of any token store if required. - * - * @param {String} token The token to get the documents for. - * @param {String} ref The ref of the document to remove from this token. - * @param {Object} root An optional node at which to start. - * @returns {Object} - * @memberOf TokenStore - */ - lunr.TokenStore.prototype.remove = function (token, ref) { - if (!token) return - var node = this.root - - for (var i = 0; i < token.length; i++) { - if (!(token[i] in node)) return - node = node[token[i]] - } - - delete node.docs[ref] - } - - /** - * Find all the possible suffixes of the passed token using tokens - * currently in the store. - * - * @param {String} token The token to expand. - * @returns {Array} - * @memberOf TokenStore - */ - lunr.TokenStore.prototype.expand = function (token, memo) { - var root = this.getNode(token), - docs = root.docs || {}, - memo = memo || [] - - if (Object.keys(docs).length) memo.push(token) - - Object.keys(root) - .forEach(function (key) { - if (key === 'docs') return - - memo.concat(this.expand(token + key, memo)) - }, this) - - return memo - } - - /** - * Returns a representation of the token store ready for serialisation. - * - * @returns {Object} - * @memberOf TokenStore - */ - lunr.TokenStore.prototype.toJSON = function () { - return { - root: this.root, - length: this.length - } - } - - - /** - * export the module via AMD, CommonJS or as a browser global - * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js - */ - ;(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(factory) - } else if (typeof exports === 'object') { - /** - * Node. Does not work with strict CommonJS, but - * only CommonJS-like enviroments that support module.exports, - * like Node. - */ - module.exports = factory() - } else { - // Browser globals (root is window) - root.lunr = factory() - } - }(this, function () { - /** - * Just return a value to define the module export. - * This example returns an object, but the module - * can return a function as the exported value. - */ - return lunr - })) -})() diff --git a/samples/rest-notes-slate/slate/source/layouts/layout.erb b/samples/rest-notes-slate/slate/source/layouts/layout.erb deleted file mode 100644 index 8d08fc2a4..000000000 --- a/samples/rest-notes-slate/slate/source/layouts/layout.erb +++ /dev/null @@ -1,102 +0,0 @@ -<%# -Copyright 2008-2013 Concur Technologies, Inc. - -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. -%> -<% language_tabs = current_page.data.language_tabs %> - - - - - - - <%= current_page.data.title || "API Documentation" %> - - <%= stylesheet_link_tag :screen, media: :screen %> - <%= stylesheet_link_tag :print, media: :print %> - - <% if current_page.data.search %> - <%= javascript_include_tag "all" %> - <% else %> - <%= javascript_include_tag "all_nosearch" %> - <% end %> - - <% if language_tabs %> - - <% end %> - - - - - - NAV - <%= image_tag('navbar.png') %> - - -
        - <%= image_tag "logo.png" %> - <% if language_tabs %> -
        - <% language_tabs.each do |lang| %> - <% if lang.is_a? Hash %> - <%= lang.values.first %> - <% else %> - <%= lang %> - <% end %> - <% end %> -
        - <% end %> - <% if current_page.data.search %> - -
          - <% end %> -
          -
          - <% if current_page.data.toc_footers %> - - <% end %> -
          -
          -
          -
          - <%= yield %> - <% current_page.data.includes && current_page.data.includes.each do |include| %> - <%= partial "includes/#{include}" %> - <% end %> -
          -
          - <% if language_tabs %> -
          - <% language_tabs.each do |lang| %> - <% if lang.is_a? Hash %> - <%= lang.values.first %> - <% else %> - <%= lang %> - <% end %> - <% end %> -
          - <% end %> -
          -
          - - diff --git a/samples/rest-notes-slate/slate/source/stylesheets/_icon-font.scss b/samples/rest-notes-slate/slate/source/stylesheets/_icon-font.scss deleted file mode 100644 index b59948398..000000000 --- a/samples/rest-notes-slate/slate/source/stylesheets/_icon-font.scss +++ /dev/null @@ -1,38 +0,0 @@ -@font-face { - font-family: 'slate'; - src:font-url('slate.eot?-syv14m'); - src:font-url('slate.eot?#iefix-syv14m') format('embedded-opentype'), - font-url('slate.woff2?-syv14m') format('woff2'), - font-url('slate.woff?-syv14m') format('woff'), - font-url('slate.ttf?-syv14m') format('truetype'), - font-url('slate.svg?-syv14m#slate') format('svg'); - font-weight: normal; - font-style: normal; -} - -%icon { - font-family: 'slate'; - speak: none; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; -} - -%icon-exclamation-sign { - @extend %icon; - content: "\e600"; -} -%icon-info-sign { - @extend %icon; - content: "\e602"; -} -%icon-ok-sign { - @extend %icon; - content: "\e606"; -} -%icon-search { - @extend %icon; - content: "\e607"; -} diff --git a/samples/rest-notes-slate/slate/source/stylesheets/_normalize.css b/samples/rest-notes-slate/slate/source/stylesheets/_normalize.css deleted file mode 100644 index 46f646a5c..000000000 --- a/samples/rest-notes-slate/slate/source/stylesheets/_normalize.css +++ /dev/null @@ -1,427 +0,0 @@ -/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ - -/** - * 1. Set default font family to sans-serif. - * 2. Prevent iOS text size adjust after orientation change, without disabling - * user zoom. - */ - -html { - font-family: sans-serif; /* 1 */ - -ms-text-size-adjust: 100%; /* 2 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/** - * Remove default margin. - */ - -body { - margin: 0; -} - -/* HTML5 display definitions - ========================================================================== */ - -/** - * Correct `block` display not defined for any HTML5 element in IE 8/9. - * Correct `block` display not defined for `details` or `summary` in IE 10/11 - * and Firefox. - * Correct `block` display not defined for `main` in IE 11. - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} - -/** - * 1. Correct `inline-block` display not defined in IE 8/9. - * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. - */ - -audio, -canvas, -progress, -video { - display: inline-block; /* 1 */ - vertical-align: baseline; /* 2 */ -} - -/** - * Prevent modern browsers from displaying `audio` without controls. - * Remove excess height in iOS 5 devices. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Address `[hidden]` styling not present in IE 8/9/10. - * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. - */ - -[hidden], -template { - display: none; -} - -/* Links - ========================================================================== */ - -/** - * Remove the gray background color from active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * Improve readability when focused and also mouse hovered in all browsers. - */ - -a:active, -a:hover { - outline: 0; -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Address styling not present in IE 8/9/10/11, Safari, and Chrome. - */ - -abbr[title] { - border-bottom: 1px dotted; -} - -/** - * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. - */ - -b, -strong { - font-weight: bold; -} - -/** - * Address styling not present in Safari and Chrome. - */ - -dfn { - font-style: italic; -} - -/** - * Address variable `h1` font-size and margin within `section` and `article` - * contexts in Firefox 4+, Safari, and Chrome. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/** - * Address styling not present in IE 8/9. - */ - -mark { - background: #ff0; - color: #000; -} - -/** - * Address inconsistent and variable font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` affecting `line-height` in all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove border when inside `a` element in IE 8/9/10. - */ - -img { - border: 0; -} - -/** - * Correct overflow not hidden in IE 9/10/11. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Grouping content - ========================================================================== */ - -/** - * Address margin not present in IE 8/9 and Safari. - */ - -figure { - margin: 1em 40px; -} - -/** - * Address differences between Firefox and other browsers. - */ - -hr { - -moz-box-sizing: content-box; - box-sizing: content-box; - height: 0; -} - -/** - * Contain overflow in all browsers. - */ - -pre { - overflow: auto; -} - -/** - * Address odd `em`-unit font size rendering in all browsers. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} - -/* Forms - ========================================================================== */ - -/** - * Known limitation: by default, Chrome and Safari on OS X allow very limited - * styling of `select`, unless a `border` property is set. - */ - -/** - * 1. Correct color not being inherited. - * Known issue: affects color of disabled elements. - * 2. Correct font properties not being inherited. - * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. - */ - -button, -input, -optgroup, -select, -textarea { - color: inherit; /* 1 */ - font: inherit; /* 2 */ - margin: 0; /* 3 */ -} - -/** - * Address `overflow` set to `hidden` in IE 8/9/10/11. - */ - -button { - overflow: visible; -} - -/** - * Address inconsistent `text-transform` inheritance for `button` and `select`. - * All other form control elements do not inherit `text-transform` values. - * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. - * Correct `select` style inheritance in Firefox. - */ - -button, -select { - text-transform: none; -} - -/** - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` - * and `video` controls. - * 2. Correct inability to style clickable `input` types in iOS. - * 3. Improve usability and consistency of cursor style between image-type - * `input` and others. - */ - -button, -html input[type="button"], /* 1 */ -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; /* 2 */ - cursor: pointer; /* 3 */ -} - -/** - * Re-set default cursor for disabled elements. - */ - -button[disabled], -html input[disabled] { - cursor: default; -} - -/** - * Remove inner padding and border in Firefox 4+. - */ - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/** - * Address Firefox 4+ setting `line-height` on `input` using `!important` in - * the UA stylesheet. - */ - -input { - line-height: normal; -} - -/** - * It's recommended that you don't attempt to style these elements. - * Firefox's implementation doesn't respect box-sizing, padding, or width. - * - * 1. Address box sizing set to `content-box` in IE 8/9/10. - * 2. Remove excess padding in IE 8/9/10. - */ - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Fix the cursor style for Chrome's increment/decrement buttons. For certain - * `font-size` values of the `input`, it causes the cursor style of the - * decrement button to change from `default` to `text`. - */ - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Address `appearance` set to `searchfield` in Safari and Chrome. - * 2. Address `box-sizing` set to `border-box` in Safari and Chrome - * (include `-moz` to future-proof). - */ - -input[type="search"] { - -webkit-appearance: textfield; /* 1 */ - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; /* 2 */ - box-sizing: content-box; -} - -/** - * Remove inner padding and search cancel button in Safari and Chrome on OS X. - * Safari (but not Chrome) clips the cancel button when the search input has - * padding (and `textfield` appearance). - */ - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * Define consistent border, margin, and padding. - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct `color` not being inherited in IE 8/9/10/11. - * 2. Remove padding so people aren't caught out if they zero out fieldsets. - */ - -legend { - border: 0; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Remove default vertical scrollbar in IE 8/9/10/11. - */ - -textarea { - overflow: auto; -} - -/** - * Don't inherit the `font-weight` (applied by a rule above). - * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. - */ - -optgroup { - font-weight: bold; -} - -/* Tables - ========================================================================== */ - -/** - * Remove most spacing between table cells. - */ - -table { - border-collapse: collapse; - border-spacing: 0; -} - -td, -th { - padding: 0; -} diff --git a/samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb b/samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb deleted file mode 100644 index 6874dcd9f..000000000 --- a/samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2008-2013 Concur Technologies, Inc. - -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. -*/ - -@import 'variables'; - -<%= Rouge::Themes::Base16::Monokai.render(:scope => '.highlight') %> - -.highlight .c, .highlight .cm, .highlight .c1, .highlight .cs { - color: #909090; -} - -.highlight, .highlight .w { - background-color: $code-bg; -} \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/stylesheets/_variables.scss b/samples/rest-notes-slate/slate/source/stylesheets/_variables.scss deleted file mode 100644 index 528996ca6..000000000 --- a/samples/rest-notes-slate/slate/source/stylesheets/_variables.scss +++ /dev/null @@ -1,109 +0,0 @@ -/* -Copyright 2008-2013 Concur Technologies, Inc. - -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. -*/ - - -//////////////////////////////////////////////////////////////////////////////// -// CUSTOMIZE SLATE -//////////////////////////////////////////////////////////////////////////////// -// Use these settings to help adjust the appearance of Slate - - -// BACKGROUND COLORS -//////////////////// -$nav-bg: #393939; -$examples-bg: #393939; -$code-bg: #292929; -$code-annotation-bg: #1c1c1c; -$nav-subitem-bg: #262626; -$nav-active-bg: #2467af; -$lang-select-border: #000; -$lang-select-bg: #222; -$lang-select-active-bg: $examples-bg; // feel free to change this to blue or something -$lang-select-pressed-bg: #111; // color of language tab bg when mouse is pressed -$main-bg: #eaf2f6; -$aside-notice-bg: #8fbcd4; -$aside-warning-bg: #c97a7e; -$aside-success-bg: #6ac174; -$search-notice-bg: #c97a7e; - - -// TEXT COLORS -//////////////////// -$main-text: #333; // main content text color -$nav-text: #fff; -$nav-active-text: #fff; -$lang-select-text: #fff; // color of unselected language tab text -$lang-select-active-text: #fff; // color of selected language tab text -$lang-select-pressed-text: #fff; // color of language tab text when mouse is pressed - - -// SIZES -//////////////////// -$nav-width: 230px; // width of the navbar -$examples-width: 50%; // portion of the screen taken up by code examples -$logo-margin: 20px; // margin between nav items and logo, ignored if search is active -$main-padding: 28px; // padding to left and right of content & examples -$nav-padding: 15px; // padding to left and right of navbar -$nav-v-padding: 10px; // padding used vertically around search boxes and results -$nav-indent: 10px; // extra padding for ToC subitems -$code-annotation-padding: 13px; // padding inside code annotations -$h1-margin-bottom: 21px; // padding under the largest header tags -$tablet-width: 930px; // min width before reverting to tablet size -$phone-width: $tablet-width - $nav-width; // min width before reverting to mobile size - - -// FONTS -//////////////////// -%default-font { - font-family: "Helvetica Neue", Helvetica, Arial, "Microsoft Yahei","微软雅黑", STXihei, "华文细黑", sans-serif; - font-size: 13px; -} - -%header-font { - @extend %default-font; - font-weight: bold; -} - -%code-font { - font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, serif; - font-size: 12px; - line-height: 1.5; -} - - -// OTHER -//////////////////// -$nav-active-shadow: #000; -$nav-footer-border-color: #666; -$nav-embossed-border-top: #000; -$nav-embossed-border-bottom: #939393; -$main-embossed-text-shadow: 0px 1px 0px #fff; -$search-box-border-color: #666; - - -//////////////////////////////////////////////////////////////////////////////// -// INTERNAL -//////////////////////////////////////////////////////////////////////////////// -// These settings are probably best left alone. - -%break-words { - word-break: break-all; - - /* Non standard for webkit */ - word-break: break-word; - - hyphens: auto; -} diff --git a/samples/rest-notes-slate/slate/source/stylesheets/print.css.scss b/samples/rest-notes-slate/slate/source/stylesheets/print.css.scss deleted file mode 100644 index 6a73fd8eb..000000000 --- a/samples/rest-notes-slate/slate/source/stylesheets/print.css.scss +++ /dev/null @@ -1,142 +0,0 @@ -@charset "utf-8"; -@import 'normalize'; -@import 'compass'; -@import 'variables'; -@import 'icon-font'; - -/* -Copyright 2008-2013 Concur Technologies, Inc. - -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. -*/ - -$print-color: #999; -$print-color-light: #ccc; -$print-font-size: 12px; - -body { - @extend %default-font; -} - -.tocify, .toc-footer, .lang-selector, .search, #nav-button { - display: none; -} - -.tocify-wrapper>img { - margin: 0 auto; - display: block; -} - -.content { - font-size: 12px; - - pre, code { - @extend %code-font; - @extend %break-words; - border: 1px solid $print-color; - border-radius: 5px; - font-size: 0.8em; - } - - pre { - padding: 1.3em; - } - - code { - padding: 0.2em; - } - - table { - border: 1px solid $print-color; - tr { - border-bottom: 1px solid $print-color; - } - td,th { - padding: 0.7em; - } - } - - p { - line-height: 1.5; - } - - a { - text-decoration: none; - color: #000; - } - - h1 { - @extend %header-font; - font-size: 2.5em; - padding-top: 0.5em; - padding-bottom: 0.5em; - margin-top: 1em; - margin-bottom: $h1-margin-bottom; - border: 2px solid $print-color-light; - border-width: 2px 0; - text-align: center; - } - - h2 { - @extend %header-font; - font-size: 1.8em; - margin-top: 2em; - border-top: 2px solid $print-color-light; - padding-top: 0.8em; - } - - h1+h2, h1+div+h2 { - border-top: none; - padding-top: 0; - margin-top: 0; - } - - h3, h4 { - @extend %header-font; - font-size: 0.8em; - margin-top: 1.5em; - margin-bottom: 0.8em; - text-transform: uppercase; - } - - h5, h6 { - text-transform: uppercase; - } - - aside { - padding: 1em; - border: 1px solid $print-color-light; - border-radius: 5px; - margin-top: 1.5em; - margin-bottom: 1.5em; - line-height: 1.6; - } - - aside:before { - vertical-align: middle; - padding-right: 0.5em; - font-size: 14px; - } - - aside.notice:before { - @extend %icon-info-sign; - } - - aside.warning:before { - @extend %icon-exclamation-sign; - } - - aside.success:before { - @extend %icon-ok-sign; - } -} \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/stylesheets/screen.css.scss b/samples/rest-notes-slate/slate/source/stylesheets/screen.css.scss deleted file mode 100644 index 2a01c14ef..000000000 --- a/samples/rest-notes-slate/slate/source/stylesheets/screen.css.scss +++ /dev/null @@ -1,620 +0,0 @@ -@charset "utf-8"; -@import 'normalize'; -@import 'compass'; -@import 'variables'; -@import 'syntax'; -@import 'icon-font'; - -/* -Copyright 2008-2013 Concur Technologies, Inc. - -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. -*/ - -//////////////////////////////////////////////////////////////////////////////// -// GENERAL STUFF -//////////////////////////////////////////////////////////////////////////////// - -html, body { - color: $main-text; - padding: 0; - margin: 0; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - @extend %default-font; - background-color: $main-bg; - height: 100%; - -webkit-text-size-adjust: none; /* Never autoresize text */ -} - -//////////////////////////////////////////////////////////////////////////////// -// TABLE OF CONTENTS -//////////////////////////////////////////////////////////////////////////////// - -#toc > ul > li > a > span { - float: right; - background-color: #2484FF; - border-radius: 40px; - width: 20px; -} - -@mixin embossed-bg { - background: - linear-gradient(to bottom, rgba(#000, 0.2), rgba(#000, 0) 8px), - linear-gradient(to top, rgba(#000, 0.2), rgba(#000, 0) 8px), - linear-gradient(to bottom, rgba($nav-embossed-border-top, 1), rgba($nav-embossed-border-top, 0) 1.5px), - linear-gradient(to top, rgba($nav-embossed-border-bottom, 1), rgba($nav-embossed-border-bottom, 0) 1.5px), - $nav-subitem-bg; -} - -.tocify-wrapper { - transition: left 0.3s ease-in-out; - - overflow-y: auto; - overflow-x: hidden; - position: fixed; - z-index: 30; - top: 0; - left: 0; - bottom: 0; - width: $nav-width; - background-color: $nav-bg; - font-size: 13px; - font-weight: bold; - - // language selector for mobile devices - .lang-selector { - display: none; - a { - padding-top: 0.5em; - padding-bottom: 0.5em; - } - } - - // This is the logo at the top of the ToC - &>img { - display: block; - } - - &>.search { - position: relative; - - input { - background: $nav-bg; - border-width: 0 0 1px 0; - border-color: $search-box-border-color; - padding: 6px 0 6px 20px; - box-sizing: border-box; - margin: $nav-v-padding $nav-padding; - width: $nav-width - 30; - outline: none; - color: $nav-text; - border-radius: 0; /* ios has a default border radius */ - } - - &:before { - position: absolute; - top: 17px; - left: $nav-padding; - color: $nav-text; - @extend %icon-search; - } - } - - img+.tocify { - margin-top: $logo-margin; - } - - .search-results { - margin-top: 0; - box-sizing: border-box; - height: 0; - overflow-y: auto; - overflow-x: hidden; - transition-property: height, margin; - transition-duration: 180ms; - transition-timing-function: ease-in-out; - &.visible { - height: 30%; - margin-bottom: 1em; - } - - @include embossed-bg; - - li { - margin: 1em $nav-padding; - line-height: 1; - } - - a { - color: $nav-text; - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } - } - - - .tocify-item>a, .toc-footer li { - padding: 0 $nav-padding 0 $nav-padding; - display: block; - overflow-x: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - // The Table of Contents is composed of multiple nested - // unordered lists. These styles remove the default - // styling of an unordered list because it is ugly. - ul, li { - list-style: none; - margin: 0; - padding: 0; - line-height: 28px; - } - - li { - color: $nav-text; - transition-property: background; - transition-timing-function: linear; - transition-duration: 230ms; - } - - // This is the currently selected ToC entry - .tocify-focus { - box-shadow: 0px 1px 0px $nav-active-shadow; - background-color: $nav-active-bg; - color: $nav-active-text; - } - - // Subheaders are the submenus that slide open - // in the table of contents. - .tocify-subheader { - display: none; // tocify will override this when needed - background-color: $nav-subitem-bg; - font-weight: 500; - .tocify-item>a { - padding-left: $nav-padding + $nav-indent; - font-size: 12px; - } - - // for embossed look: - @include embossed-bg; - &>li:last-child { - box-shadow: none; // otherwise it'll overflow out of the subheader - } - } - - .toc-footer { - padding: 1em 0; - margin-top: 1em; - border-top: 1px dashed $nav-footer-border-color; - - li,a { - color: $nav-text; - text-decoration: none; - } - - a:hover { - text-decoration: underline; - } - - li { - font-size: 0.8em; - line-height: 1.7; - text-decoration: none; - } - } - -} - -// button to show navigation on mobile devices -#nav-button { - span { - display: block; - $side-pad: $main-padding / 2 - 8px; - padding: $side-pad $side-pad $side-pad; - background-color: rgba($main-bg, 0.7); - transform-origin: 0 0; - transform: rotate(-90deg) translate(-100%, 0); - border-radius: 0 0 0 5px; - } - padding: 0 1.5em 5em 0; // increase touch size area - display: none; - position: fixed; - top: 0; - left: 0; - z-index: 100; - color: #000; - text-decoration: none; - font-weight: bold; - opacity: 0.7; - line-height: 16px; - img { - height: 16px; - vertical-align: bottom; - } - - transition: left 0.3s ease-in-out; - - &:hover { opacity: 1; } - &.open {left: $nav-width} -} - - -//////////////////////////////////////////////////////////////////////////////// -// PAGE LAYOUT AND CODE SAMPLE BACKGROUND -//////////////////////////////////////////////////////////////////////////////// - -.page-wrapper { - margin-left: $nav-width; - position: relative; - z-index: 10; - background-color: $main-bg; - min-height: 100%; - - padding-bottom: 1px; // prevent margin overflow - - // The dark box is what gives the code samples their dark background. - // It sits essentially under the actual content block, which has a - // transparent background. - // I know, it's hackish, but it's the simplist way to make the left - // half of the content always this background color. - .dark-box { - width: $examples-width; - background-color: $examples-bg; - position: absolute; - right: 0; - top: 0; - bottom: 0; - } - - .lang-selector { - position: fixed; - z-index: 50; - border-bottom: 5px solid $lang-select-active-bg; - } -} - -.lang-selector { - background-color: $lang-select-bg; - width: 100%; - font-weight: bold; - a { - display: block; - float:left; - color: $lang-select-text; - text-decoration: none; - padding: 0 10px; - line-height: 30px; - outline: 0; - - &:active, &:focus { - background-color: $lang-select-pressed-bg; - color: $lang-select-pressed-text; - } - - &.active { - background-color: $lang-select-active-bg; - color: $lang-select-active-text; - } - } - - &:after { - content: ''; - clear: both; - display: block; - } -} - -//////////////////////////////////////////////////////////////////////////////// -// CONTENT STYLES -//////////////////////////////////////////////////////////////////////////////// -// This is all the stuff with the light background in the left half of the page - -.content { - // to place content above the dark box - position: relative; - z-index: 30; - - &:after { - content: ''; - display: block; - clear: both; - } - - &>h1, &>h2, &>h3, &>h4, &>h5, &>h6, &>p, &>table, &>ul, &>ol, &>aside, &>dl { - margin-right: $examples-width; - padding: 0 $main-padding; - box-sizing: border-box; - display: block; - @include text-shadow($main-embossed-text-shadow); - - @extend %left-col; - } - - &>ul, &>ol { - padding-left: $main-padding + 15px; - } - - // the div is the tocify hidden div for placeholding stuff - &>h1, &>h2, &>div { - clear:both; - } - - h1 { - @extend %header-font; - font-size: 30px; - padding-top: 0.5em; - padding-bottom: 0.5em; - border-bottom: 1px solid #ccc; - margin-bottom: $h1-margin-bottom; - margin-top: 2em; - border-top: 1px solid #ddd; - background-image: linear-gradient(to bottom, #fff, #f9f9f9); - } - - h1:first-child, div:first-child + h1 { - border-top-width: 0; - margin-top: 0; - } - - h2 { - @extend %header-font; - font-size: 20px; - margin-top: 4em; - margin-bottom: 0; - border-top: 1px solid #ccc; - padding-top: 1.2em; - padding-bottom: 1.2em; - background-image: linear-gradient(to bottom, rgba(#fff, 0.4), rgba(#fff, 0)); - } - - // h2s right after h1s should bump right up - // against the h1s. - h1 + h2, h1 + div + h2 { - margin-top: $h1-margin-bottom * -1; - border-top: none; - } - - h3, h4, h5, h6 { - @extend %header-font; - font-size: 15px; - margin-top: 2.5em; - margin-bottom: 0.8em; - } - - h4, h5, h6 { - font-size: 10px; - } - - hr { - margin: 2em 0; - border-top: 2px solid $examples-bg; - border-bottom: 2px solid $main-bg; - } - - table { - margin-bottom: 1em; - overflow: auto; - th,td { - text-align: left; - vertical-align: top; - line-height: 1.6; - } - - th { - padding: 5px 10px; - border-bottom: 1px solid #ccc; - vertical-align: bottom; - } - - td { - padding: 10px; - } - - tr:last-child { - border-bottom: 1px solid #ccc; - } - - tr:nth-child(odd)>td { - background-color: lighten($main-bg,4.2%); - } - - tr:nth-child(even)>td { - background-color: lighten($main-bg,2.4%); - } - } - - dt { - font-weight: bold; - } - - dd { - margin-left: 15px; - } - - p, li, dt, dd { - line-height: 1.6; - margin-top: 0; - } - - img { - max-width: 100%; - } - - code { - background-color: rgba(0,0,0,0.05); - padding: 3px; - border-radius: 3px; - @extend %break-words; - @extend %code-font; - } - - pre>code { - background-color: transparent; - padding: 0; - } - - aside { - padding-top: 1em; - padding-bottom: 1em; - @include text-shadow(0 1px 0 lighten($aside-notice-bg, 15%)); - margin-top: 1.5em; - margin-bottom: 1.5em; - background: $aside-notice-bg; - line-height: 1.6; - - &.warning { - background-color: $aside-warning-bg; - @include text-shadow(0 1px 0 lighten($aside-warning-bg, 15%)); - } - - &.success { - background-color: $aside-success-bg; - @include text-shadow(0 1px 0 lighten($aside-success-bg, 15%)); - } - } - - aside:before { - vertical-align: middle; - padding-right: 0.5em; - font-size: 14px; - } - - aside.notice:before { - @extend %icon-info-sign; - } - - aside.warning:before { - @extend %icon-exclamation-sign; - } - - aside.success:before { - @extend %icon-ok-sign; - } - - .search-highlight { - padding: 2px; - margin: -2px; - border-radius: 4px; - border: 1px solid #F7E633; - @include text-shadow(1px 1px 0 #666); - background: linear-gradient(to top left, #F7E633 0%, #F1D32F 100%); - } -} - -//////////////////////////////////////////////////////////////////////////////// -// CODE SAMPLE STYLES -//////////////////////////////////////////////////////////////////////////////// -// This is all the stuff that appears in the right half of the page - -.content { - pre, blockquote { - background-color: $code-bg; - color: #fff; - - padding: 2em $main-padding; - margin: 0; - width: $examples-width; - - float:right; - clear:right; - - box-sizing: border-box; - @include text-shadow(0px 1px 2px rgba(0,0,0,0.4)); - - @extend %right-col; - - &>p { margin: 0; } - - a { - color: #fff; - text-decoration: none; - border-bottom: dashed 1px #ccc; - } - } - - pre { - @extend %code-font; - } - - blockquote { - &>p { - background-color: $code-annotation-bg; - border-radius: 5px; - padding: $code-annotation-padding; - color: #ccc; - border-top: 1px solid #000; - border-bottom: 1px solid #404040; - } - } -} - -//////////////////////////////////////////////////////////////////////////////// -// RESPONSIVE DESIGN -//////////////////////////////////////////////////////////////////////////////// -// These are the styles for phones and tablets -// There are also a couple styles disperesed - -@media (max-width: $tablet-width) { - .tocify-wrapper { - left: -$nav-width; - - &.open { - left: 0; - } - } - - .page-wrapper { - margin-left: 0; - } - - #nav-button { - display: block; - } - - .tocify-wrapper .tocify-item > a { - padding-top: 0.3em; - padding-bottom: 0.3em; - } -} - -@media (max-width: $phone-width) { - .dark-box { - display: none; - } - - %left-col { - margin-right: 0; - } - - .tocify-wrapper .lang-selector { - display: block; - } - - .page-wrapper .lang-selector { - display: none; - } - - %right-col { - width: auto; - float: none; - } - - %right-col + %left-col { - margin-top: $main-padding; - } -} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java b/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java deleted file mode 100644 index 53104ede7..000000000 --- a/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example.notes; - -import java.util.List; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -@Entity -public class Note { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private long id; - - private String title; - - private String body; - - @ManyToMany - private List tags; - - @JsonIgnore - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getBody() { - return body; - } - - public void setBody(String body) { - this.body = body; - } - - public List getTags() { - return tags; - } - - public void setTags(List tags) { - this.tags = tags; - } -} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/NoteRepository.java b/samples/rest-notes-slate/src/main/java/com/example/notes/NoteRepository.java deleted file mode 100644 index b7a26a94b..000000000 --- a/samples/rest-notes-slate/src/main/java/com/example/notes/NoteRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example.notes; - -import org.springframework.data.repository.CrudRepository; - -public interface NoteRepository extends CrudRepository { - -} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java b/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java deleted file mode 100644 index e20e4feca..000000000 --- a/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example.notes; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class RestNotesSlate { - - public static void main(String[] args) { - SpringApplication.run(RestNotesSlate.class, args); - } - -} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java b/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java deleted file mode 100644 index 8bd834f98..000000000 --- a/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import java.util.List; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -@Entity -public class Tag { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private long id; - - private String name; - - @ManyToMany(mappedBy = "tags") - private List notes; - - @JsonIgnore - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getNotes() { - return notes; - } - - public void setNotes(List notes) { - this.notes = notes; - } -} diff --git a/samples/rest-notes-slate/src/main/resources/application.properties b/samples/rest-notes-slate/src/main/resources/application.properties deleted file mode 100644 index 8e06a8284..000000000 --- a/samples/rest-notes-slate/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.jackson.serialization.indent_output: true \ No newline at end of file diff --git a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java deleted file mode 100644 index ef702fa15..000000000 --- a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example.notes; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.restdocs.templates.TemplateFormats.markdown; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.RequestDispatcher; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import com.fasterxml.jackson.databind.ObjectMapper; - -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSlate.class) -@WebAppConfiguration -public class ApiDocumentation { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); - - @Autowired - private NoteRepository noteRepository; - - @Autowired - private TagRepository tagRepository; - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private WebApplicationContext context; - - private MockMvc mockMvc; - - @Before - public void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).snippets() - .withTemplateFormat(markdown())).build(); - } - - @Test - public void errorExample() throws Exception { - this.mockMvc - .perform(get("/error") - .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) - .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, - "/notes") - .requestAttr(RequestDispatcher.ERROR_MESSAGE, - "The tag 'http://localhost:8080/tags/123' does not exist")) - .andDo(print()).andExpect(status().isBadRequest()) - .andExpect(jsonPath("error", is("Bad Request"))) - .andExpect(jsonPath("timestamp", is(notNullValue()))) - .andExpect(jsonPath("status", is(400))) - .andExpect(jsonPath("path", is(notNullValue()))) - .andDo(document("error-example", - responseFields( - fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"), - fieldWithPath("message").description("A description of the cause of the error"), - fieldWithPath("path").description("The path to which the request was made"), - fieldWithPath("status").description("The HTTP status code, e.g. `400`"), - fieldWithPath("timestamp").description("The time, in milliseconds, at which the error occurred")))); - } - - @Test - public void indexExample() throws Exception { - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andDo(document("index-example", - links( - linkWithRel("notes").description("The [Notes](#notes) resource"), - linkWithRel("tags").description("The [Tags](#tags) resource"), - linkWithRel("profile").description("The ALPS profile for the service")), - responseFields( - fieldWithPath("_links").description("Links to other resources")))); - - } - - @Test - public void notesListExample() throws Exception { - this.noteRepository.deleteAll(); - - createNote("REST maturity model", - "https://martinfowler.com/articles/richardsonMaturityModel.html"); - createNote("Hypertext Application Language (HAL)", - "https://github.com/mikekelly/hal_specification"); - createNote("Application-Level Profile Semantics (ALPS)", "https://github.com/alps-io/spec"); - - this.mockMvc.perform(get("/notes")) - .andExpect(status().isOk()) - .andDo(document("notes-list-example", - responseFields( - fieldWithPath("_embedded.notes").description("An array of [Note](#note) resources")))); - } - - @Test - public void notesCreateExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - Map note = new HashMap(); - note.put("title", "REST maturity model"); - note.put("body", "https://martinfowler.com/articles/richardsonMaturityModel.html"); - note.put("tags", Arrays.asList(tagLocation)); - - this.mockMvc.perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))).andExpect( - status().isCreated()) - .andDo(document("notes-create-example", - requestFields( - fieldWithPath("title").description("The title of the note"), - fieldWithPath("body").description("The body of the note"), - fieldWithPath("tags").description("An array of [Tag](#tag) resource URIs")))); - } - - @Test - public void noteGetExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - Map note = new HashMap(); - note.put("title", "REST maturity model"); - note.put("body", "https://martinfowler.com/articles/richardsonMaturityModel.html"); - note.put("tags", Arrays.asList(tagLocation)); - - String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - this.mockMvc.perform(get(noteLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(note.get("title")))) - .andExpect(jsonPath("body", is(note.get("body")))) - .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))) - .andDo(document("note-get-example", - links( - linkWithRel("self").description("This note"), - linkWithRel("tags").description("This note's tags")), - responseFields( - fieldWithPath("title").description("The title of the note"), - fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("Links to other resources")))); - } - - @Test - public void tagsListExample() throws Exception { - this.noteRepository.deleteAll(); - this.tagRepository.deleteAll(); - - createTag("REST"); - createTag("Hypermedia"); - createTag("HTTP"); - - this.mockMvc.perform(get("/tags")) - .andExpect(status().isOk()) - .andDo(document("tags-list-example", - responseFields( - fieldWithPath("_embedded.tags").description("An array of [Tag](#tag) resources")))); - } - - @Test - public void tagsCreateExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - this.mockMvc.perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andDo(document("tags-create-example", - requestFields( - fieldWithPath("name").description("The name of the tag")))); - } - - @Test - public void noteUpdateExample() throws Exception { - Map note = new HashMap(); - note.put("title", "REST maturity model"); - note.put("body", "https://martinfowler.com/articles/richardsonMaturityModel.html"); - - String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("title", is(note.get("title")))) - .andExpect(jsonPath("body", is(note.get("body")))) - .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))); - - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - Map noteUpdate = new HashMap(); - noteUpdate.put("tags", Arrays.asList(tagLocation)); - - this.mockMvc.perform( - patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(noteUpdate))) - .andExpect(status().isNoContent()) - .andDo(document("note-update-example", - requestFields( - fieldWithPath("title").description("The title of the note").type(JsonFieldType.STRING).optional(), - fieldWithPath("body").description("The body of the note").type(JsonFieldType.STRING).optional(), - fieldWithPath("tags").description("An array of [tag](#tag) resource URIs").optional()))); - } - - @Test - public void tagGetExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - this.mockMvc.perform(get(tagLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("name", is(tag.get("name")))) - .andDo(document("tag-get-example", - links( - linkWithRel("self").description("This tag"), - linkWithRel("notes").description("The notes that have this tag")), - responseFields( - fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("Links to other resources")))); - } - - @Test - public void tagUpdateExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - Map tagUpdate = new HashMap(); - tagUpdate.put("name", "RESTful"); - - this.mockMvc.perform( - patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tagUpdate))) - .andExpect(status().isNoContent()) - .andDo(document("tag-update-example", - requestFields( - fieldWithPath("name").description("The name of the tag")))); - } - - private void createNote(String title, String body) { - Note note = new Note(); - note.setTitle(title); - note.setBody(body); - - this.noteRepository.save(note); - } - - private void createTag(String name) { - Tag tag = new Tag(); - tag.setName(name); - this.tagRepository.save(tag); - } -} diff --git a/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar b/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index c6feb8bb6..000000000 Binary files a/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties b/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index eb9194764..000000000 --- a/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1 +0,0 @@ -distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip \ No newline at end of file diff --git a/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs b/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 30f00a942..000000000 --- a/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,296 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.7 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.source=1.7 -org.eclipse.jdt.core.formatter.align_type_members_on_columns=false -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_assignment=0 -org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 -org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 -org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 -org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 -org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 -org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 -org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 -org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=1 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1 -org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 -org.eclipse.jdt.core.formatter.blank_lines_before_method=1 -org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 -org.eclipse.jdt.core.formatter.blank_lines_before_package=0 -org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 -org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 -org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false -org.eclipse.jdt.core.formatter.comment.format_block_comments=true -org.eclipse.jdt.core.formatter.comment.format_header=false -org.eclipse.jdt.core.formatter.comment.format_html=true -org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true -org.eclipse.jdt.core.formatter.comment.format_line_comments=true -org.eclipse.jdt.core.formatter.comment.format_source_code=false -org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true -org.eclipse.jdt.core.formatter.comment.indent_root_tags=false -org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=do not insert -org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert -org.eclipse.jdt.core.formatter.comment.line_length=90 -org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true -org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true -org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false -org.eclipse.jdt.core.formatter.compact_else_if=true -org.eclipse.jdt.core.formatter.continuation_indentation=2 -org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 -org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off -org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false -org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true -org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_empty_lines=false -org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true -org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false -org.eclipse.jdt.core.formatter.indentation.size=8 -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert -org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert -org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert -org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.join_lines_in_comments=true -org.eclipse.jdt.core.formatter.join_wrapped_lines=true -org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false -org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false -org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false -org.eclipse.jdt.core.formatter.lineSplit=90 -org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false -org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false -org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 -org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 -org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true -org.eclipse.jdt.core.formatter.tabulation.char=tab -org.eclipse.jdt.core.formatter.tabulation.size=4 -org.eclipse.jdt.core.formatter.use_on_off_tags=false -org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false -org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true -org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true -org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true diff --git a/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs b/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index 1f4325cd5..000000000 --- a/samples/rest-notes-spring-data-rest/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,62 +0,0 @@ -cleanup.add_default_serial_version_id=true -cleanup.add_generated_serial_version_id=false -cleanup.add_missing_annotations=true -cleanup.add_missing_deprecated_annotations=true -cleanup.add_missing_methods=false -cleanup.add_missing_nls_tags=false -cleanup.add_missing_override_annotations=true -cleanup.add_missing_override_annotations_interface_methods=true -cleanup.add_serial_version_id=false -cleanup.always_use_blocks=true -cleanup.always_use_parentheses_in_expressions=false -cleanup.always_use_this_for_non_static_field_access=true -cleanup.always_use_this_for_non_static_method_access=false -cleanup.convert_functional_interfaces=false -cleanup.convert_to_enhanced_for_loop=false -cleanup.correct_indentation=true -cleanup.format_source_code=true -cleanup.format_source_code_changes_only=false -cleanup.insert_inferred_type_arguments=false -cleanup.make_local_variable_final=false -cleanup.make_parameters_final=false -cleanup.make_private_fields_final=false -cleanup.make_type_abstract_if_missing_method=false -cleanup.make_variable_declarations_final=false -cleanup.never_use_blocks=false -cleanup.never_use_parentheses_in_expressions=true -cleanup.organize_imports=true -cleanup.qualify_static_field_accesses_with_declaring_class=false -cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -cleanup.qualify_static_member_accesses_with_declaring_class=true -cleanup.qualify_static_method_accesses_with_declaring_class=false -cleanup.remove_private_constructors=true -cleanup.remove_redundant_type_arguments=true -cleanup.remove_trailing_whitespaces=true -cleanup.remove_trailing_whitespaces_all=true -cleanup.remove_trailing_whitespaces_ignore_empty=false -cleanup.remove_unnecessary_casts=true -cleanup.remove_unnecessary_nls_tags=false -cleanup.remove_unused_imports=true -cleanup.remove_unused_local_variables=false -cleanup.remove_unused_private_fields=true -cleanup.remove_unused_private_members=false -cleanup.remove_unused_private_methods=true -cleanup.remove_unused_private_types=true -cleanup.sort_members=false -cleanup.sort_members_all=false -cleanup.use_anonymous_class_creation=false -cleanup.use_blocks=true -cleanup.use_blocks_only_for_return_and_throw=false -cleanup.use_lambda=true -cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=true -cleanup.use_this_for_non_static_field_access_only_if_necessary=false -cleanup.use_this_for_non_static_method_access=false -cleanup.use_this_for_non_static_method_access_only_if_necessary=true -cleanup.use_type_arguments=false -cleanup_profile=_Spring Rest Docs Cleanup Conventions -cleanup_settings_version=2 -eclipse.preferences.version=1 -formatter_profile=_Spring Rest Docs Java Conventions -formatter_settings_version=12 diff --git a/samples/rest-notes-spring-data-rest/mvnw b/samples/rest-notes-spring-data-rest/mvnw deleted file mode 100755 index 63fcc8f0f..000000000 --- a/samples/rest-notes-spring-data-rest/mvnw +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # - # Look for the Apple JDKs first to preserve the existing behaviour, and then look - # for the new JDKs provided by Oracle. - # - if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then - # - # Apple JDKs - # - export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home - fi - - if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then - # - # Apple JDKs - # - export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home - fi - - if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then - # - # Oracle JDKs - # - export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home - fi - - if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then - # - # Apple JDKs - # - export JAVA_HOME=`/usr/libexec/java_home` - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Migwn, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - 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 - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` -fi - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - local basedir=$(pwd) - local wdir=$(pwd) - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - wdir=$(cd "$wdir/.."; pwd) - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CMD_LINE_ARGS - diff --git a/samples/rest-notes-spring-data-rest/mvnw.cmd b/samples/rest-notes-spring-data-rest/mvnw.cmd deleted file mode 100644 index 66e928bd1..000000000 --- a/samples/rest-notes-spring-data-rest/mvnw.cmd +++ /dev/null @@ -1,145 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. 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, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -set MAVEN_CMD_LINE_ARGS=%MAVEN_CONFIG% %* - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - -set WRAPPER_JAR=""%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml deleted file mode 100644 index 311056609..000000000 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - 4.0.0 - - com.example - rest-notes-spring-data-rest - 0.0.1-SNAPSHOT - jar - - - org.springframework.boot - spring-boot-starter-parent - 1.3.6.RELEASE - - - - - UTF-8 - 1.7 - 1.1.4.BUILD-SNAPSHOT - - - - - org.springframework.boot - spring-boot-starter-data-rest - - - org.springframework.boot - spring-boot-starter-data-jpa - - - - com.h2database - h2 - runtime - - - - org.springframework.boot - spring-boot-starter-test - test - - - com.jayway.jsonpath - json-path - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - **/*Documentation.java - - - - - org.asciidoctor - asciidoctor-maven-plugin - 1.5.3 - - - generate-docs - prepare-package - - process-asciidoc - - - html - book - - ${project.build.directory}/generated-snippets - - - - - - - maven-resources-plugin - - - copy-resources - prepare-package - - copy-resources - - - ${project.build.outputDirectory}/static/docs - - - ${project.build.directory}/generated-docs - - - - - - - - - - - - spring-snapshots - Spring snapshots - https://repo.spring.io/libs-snapshot - - true - - - - - diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc deleted file mode 100644 index 45a80cb38..000000000 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc +++ /dev/null @@ -1,318 +0,0 @@ -= RESTful Notes API Guide -Andy Wilkinson; -:doctype: book -:icons: font -:source-highlighter: highlightjs -:toc: left -:toclevels: 4 -:sectlinks: - -[[overview]] -= Overview - -[[overview-http-verbs]] -== HTTP verbs - -RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP verbs. - -|=== -| Verb | Usage - -| `GET` -| Used to retrieve a resource - -| `POST` -| Used to create a new resource - -| `PATCH` -| Used to update an existing resource, including partial updates - -| `DELETE` -| Used to delete an existing resource -|=== - -[[overview-http-status-codes]] -== HTTP status codes - -RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP status codes. - -|=== -| Status code | Usage - -| `200 OK` -| The request completed successfully - -| `201 Created` -| A new resource has been created successfully. The resource's URI is available from the response's -`Location` header - -| `204 No Content` -| An update to an existing resource has been applied successfully - -| `400 Bad Request` -| The request was malformed. The response body will include an error providing further information - -| `404 Not Found` -| The requested resource did not exist -|=== - -[[overview-errors]] -== Errors - -Whenever an error response (status code >= 400) is returned, the body will contain a JSON object -that describes the problem. The error object has the following structure: - -include::{snippets}/error-example/response-fields.adoc[] - -For example, a request that attempts to apply a non-existent tag to a note will produce a -`400 Bad Request` response: - -include::{snippets}/error-example/http-response.adoc[] - -[[overview-hypermedia]] -== Hypermedia - -RESTful Notes uses hypermedia and resources include links to other resources in their -responses. Responses are in https://github.com/mikekelly/hal_specification[Hypertext -Application Language (HAL)] format. Links can be found beneath the `_links` key. Users of -the API should not create URIs themselves, instead they should use the above-described -links to navigate from resource to resource. - -[[resources]] -= Resources - - - -[[resources-index]] -== Index - -The index provides the entry point into the service. - - - -[[resources-index-access]] -=== Accessing the index - -A `GET` request is used to access the index - -==== Response structure - -include::{snippets}/index-example/response-fields.adoc[] - -==== Example response - -include::{snippets}/index-example/http-response.adoc[] - - - -[[resources-index-links]] -==== Links - -include::{snippets}/index-example/links.adoc[] - - - -[[resources-notes]] -== Notes - -The Notes resources is used to create and list notes - - - -[[resources-notes-list]] -=== Listing notes - -A `GET` request will list all of the service's notes. - -==== Response structure - -include::{snippets}/notes-list-example/response-fields.adoc[] - -==== Example request - -include::{snippets}/notes-list-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/notes-list-example/http-response.adoc[] - -[[resources-notes-list-links]] -==== Links - -include::{snippets}/notes-list-example/links.adoc[] - - -[[resources-notes-create]] -=== Creating a note - -A `POST` request is used to create a note - -==== Request structure - -include::{snippets}/notes-create-example/request-fields.adoc[] - -==== Example request - -include::{snippets}/notes-create-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/notes-create-example/http-response.adoc[] - - - -[[resources-tags]] -== Tags - -The Tags resource is used to create and list tags. - - - -[[resources-tags-list]] -=== Listing tags - -A `GET` request will list all of the service's tags. - -==== Response structure - -include::{snippets}/tags-list-example/response-fields.adoc[] - -==== Example request - -include::{snippets}/tags-list-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/tags-list-example/http-response.adoc[] - -[[resources-tags-list-links]] -==== Links - -include::{snippets}/tags-list-example/links.adoc[] - - - -[[resources-tags-create]] -=== Creating a tag - -A `POST` request is used to create a note - -==== Request structure - -include::{snippets}/tags-create-example/request-fields.adoc[] - -==== Example request - -include::{snippets}/tags-create-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/tags-create-example/http-response.adoc[] - - - -[[resources-note]] -== Note - -The Note resource is used to retrieve, update, and delete individual notes - - - -[[resources-note-links]] -=== Links - -include::{snippets}/note-get-example/links.adoc[] - - - -[[resources-note-retrieve]] -=== Retrieve a note - -A `GET` request will retrieve the details of a note - -==== Response structure - -include::{snippets}/note-get-example/response-fields.adoc[] - -==== Example request - -include::{snippets}/note-get-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/note-get-example/http-response.adoc[] - - - -[[resources-note-update]] -=== Update a note - -A `PATCH` request is used to update a note - -==== Request structure - -include::{snippets}/note-update-example/request-fields.adoc[] - -To leave an attribute of a note unchanged, any of the above may be omitted from the request. - -==== Example request - -include::{snippets}/note-update-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/note-update-example/http-response.adoc[] - - - -[[resources-tag]] -== Tag - -The Tag resource is used to retrieve, update, and delete individual tags - - - -[[resources-tag-links]] -=== Links - -include::{snippets}/tag-get-example/links.adoc[] - - - -[[resources-tag-retrieve]] -=== Retrieve a tag - -A `GET` request will retrieve the details of a tag - -==== Response structure - -include::{snippets}/tag-get-example/response-fields.adoc[] - -==== Example request - -include::{snippets}/tag-get-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/tag-get-example/http-response.adoc[] - - - -[[resources-tag-update]] -=== Update a tag - -A `PATCH` request is used to update a tag - -==== Request structure - -include::{snippets}/tag-update-example/request-fields.adoc[] - -==== Example request - -include::{snippets}/tag-update-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/tag-update-example/http-response.adoc[] diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc deleted file mode 100644 index 78a442369..000000000 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc +++ /dev/null @@ -1,178 +0,0 @@ -= RESTful Notes Getting Started Guide -Andy Wilkinson; -:doctype: book -:icons: font -:source-highlighter: highlightjs -:toc: left -:toclevels: 4 -:sectlinks: - -[introduction] -= Introduction - -RESTful Notes is a RESTful web service for creating and storing notes. It uses hypermedia -to describe the relationships between resources and to allow navigation between them. - -[getting-started] -= Getting started - - - -[getting-started-running-the-service] -== Running the service -RESTful Notes is written using https://projects.spring.io/spring-boot[Spring Boot] which -makes it easy to get it up and running so that you can start exploring the REST API. - -The first step is to clone the Git repository: - -[source,bash] ----- -$ git clone https://github.com/spring-projects/spring-restdocs ----- - -Once the clone is complete, you're ready to get the service up and running: - -[source,bash] ----- -$ cd samples/rest-notes-spring-data-rest -$ ./mvnw clean package -$ java -jar target/*.jar ----- - -You can check that the service is up and running by executing a simple request using -cURL: - -include::{snippets}/index/1/curl-request.adoc[] - -This request should yield the following response in the -https://github.com/mikekelly/hal_specification[Hypertext Application Language (HAL)] -format: - -include::{snippets}/index/1/http-response.adoc[] - -Note the `_links` in the JSON response. They are key to navigating the API. - - - -[getting-started-creating-a-note] -== Creating a note -Now that you've started the service and verified that it works, the next step is to use -it to create a new note. As you saw above, the URI for working with notes is included as -a link when you perform a `GET` request against the root of the service: - -include::{snippets}/index/1/http-response.adoc[] - -To create a note, you need to execute a `POST` request to this URI including a JSON -payload containing the title and body of the note: - -include::{snippets}/creating-a-note/1/curl-request.adoc[] - -The response from this request should have a status code of `201 Created` and contain a -`Location` header whose value is the URI of the newly created note: - -include::{snippets}/creating-a-note/1/http-response.adoc[] - -To work with the newly created note you use the URI in the `Location` header. For example, -you can access the note's details by performing a `GET` request: - -include::{snippets}/creating-a-note/2/curl-request.adoc[] - -This request will produce a response with the note's details in its body: - -include::{snippets}/creating-a-note/2/http-response.adoc[] - -Note the `tags` link which we'll make use of later. - - - -[getting-started-creating-a-tag] -== Creating a tag -To make a note easier to find, it can be associated with any number of tags. To be able -to tag a note, you must first create the tag. - -Referring back to the response for the service's index, the URI for working with tags is -include as a link: - -include::{snippets}/index/1/http-response.adoc[] - -To create a tag you need to execute a `POST` request to this URI, including a JSON -payload containing the name of the tag: - -include::{snippets}/creating-a-note/3/curl-request.adoc[] - -The response from this request should have a status code of `201 Created` and contain a -`Location` header whose value is the URI of the newly created tag: - -include::{snippets}/creating-a-note/3/http-response.adoc[] - -To work with the newly created tag you use the URI in the `Location` header. For example -you can access the tag's details by performing a `GET` request: - -include::{snippets}/creating-a-note/4/curl-request.adoc[] - -This request will produce a response with the tag's details in its body: - -include::{snippets}/creating-a-note/4/http-response.adoc[] - - - -[getting-started-tagging-a-note] -== Tagging a note -A tag isn't particularly useful until it's been associated with one or more notes. There -are two ways to tag a note: when the note is first created or by updating an existing -note. We'll look at both of these in turn. - - - -[getting-started-tagging-a-note-creating] -=== Creating a tagged note -The process is largely the same as we saw before, but this time, in addition to providing -a title and body for the note, we'll also provide the tag that we want to be associated -with it. - -Once again we execute a `POST` request. However, this time, in an array named tags, we -include the URI of the tag we just created: - -include::{snippets}/creating-a-note/5/curl-request.adoc[] - -Once again, the response's `Location` header tells us the URI of the newly created note: - -include::{snippets}/creating-a-note/5/http-response.adoc[] - -As before, a `GET` request executed against this URI will retrieve the note's details: - -include::{snippets}/creating-a-note/6/curl-request.adoc[] -include::{snippets}/creating-a-note/6/http-response.adoc[] - -To verify that the tag has been associated with the note, we can perform a `GET` request -against the URI from the `tags` link: - -include::{snippets}/creating-a-note/7/curl-request.adoc[] - -The response embeds information about the tag that we've just associated with the note: - -include::{snippets}/creating-a-note/7/http-response.adoc[] - - - -[getting-started-tagging-a-note-existing] -=== Tagging an existing note -An existing note can be tagged by executing a `PATCH` request against the note's URI with -a body that contains the array of tags to be associated with the note. We'll used the -URI of the untagged note that we created earlier: - -include::{snippets}/creating-a-note/8/curl-request.adoc[] - -This request should produce a `204 No Content` response: - -include::{snippets}/creating-a-note/8/http-response.adoc[] - -When we first created this note, we noted the tags link included in its details: - -include::{snippets}/creating-a-note/2/http-response.adoc[] - -We can use that link now and execute a `GET` request to see that the note now has a -single tag: - -include::{snippets}/creating-a-note/9/curl-request.adoc[] -include::{snippets}/creating-a-note/9/http-response.adoc[] diff --git a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Note.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Note.java deleted file mode 100644 index 5160682a3..000000000 --- a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Note.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import java.util.List; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -@Entity -public class Note { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private long id; - - private String title; - - private String body; - - @ManyToMany - private List tags; - - @JsonIgnore - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getBody() { - return body; - } - - public void setBody(String body) { - this.body = body; - } - - public List getTags() { - return tags; - } - - public void setTags(List tags) { - this.tags = tags; - } -} diff --git a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/NoteRepository.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/NoteRepository.java deleted file mode 100644 index ba6dd5fb3..000000000 --- a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/NoteRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import org.springframework.data.repository.CrudRepository; - -public interface NoteRepository extends CrudRepository { - -} diff --git a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java deleted file mode 100644 index 21195fd55..000000000 --- a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example.notes; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class RestNotesSpringDataRest { - - public static void main(String[] args) { - SpringApplication.run(RestNotesSpringDataRest.class, args); - } - -} diff --git a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java deleted file mode 100644 index 8bd834f98..000000000 --- a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/Tag.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import java.util.List; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -@Entity -public class Tag { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private long id; - - private String name; - - @ManyToMany(mappedBy = "tags") - private List notes; - - @JsonIgnore - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public List getNotes() { - return notes; - } - - public void setNotes(List notes) { - this.notes = notes; - } -} diff --git a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/TagRepository.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/TagRepository.java deleted file mode 100644 index 96ad37917..000000000 --- a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/TagRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import org.springframework.data.repository.CrudRepository; - -public interface TagRepository extends CrudRepository { - -} diff --git a/samples/rest-notes-spring-data-rest/src/main/resources/application.properties b/samples/rest-notes-spring-data-rest/src/main/resources/application.properties deleted file mode 100644 index 8e06a8284..000000000 --- a/samples/rest-notes-spring-data-rest/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.jackson.serialization.indent_output: true \ No newline at end of file diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java deleted file mode 100644 index e82868b0d..000000000 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ /dev/null @@ -1,353 +0,0 @@ -/* - * Copyright 2014-2019 the original author or 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. - */ - -package com.example.notes; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.RequestDispatcher; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import com.fasterxml.jackson.databind.ObjectMapper; - -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class) -@WebAppConfiguration -public class ApiDocumentation { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - @Autowired - private NoteRepository noteRepository; - - @Autowired - private TagRepository tagRepository; - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private WebApplicationContext context; - - private MockMvc mockMvc; - - @Before - public void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - } - - @Test - public void errorExample() throws Exception { - this.mockMvc - .perform(get("/error") - .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) - .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, - "/notes") - .requestAttr(RequestDispatcher.ERROR_MESSAGE, - "The tag 'http://localhost:8080/tags/123' does not exist")) - .andDo(print()).andExpect(status().isBadRequest()) - .andExpect(jsonPath("error", is("Bad Request"))) - .andExpect(jsonPath("timestamp", is(notNullValue()))) - .andExpect(jsonPath("status", is(400))) - .andExpect(jsonPath("path", is(notNullValue()))) - .andDo(document("error-example", - responseFields( - fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"), - fieldWithPath("message").description("A description of the cause of the error"), - fieldWithPath("path").description("The path to which the request was made"), - fieldWithPath("status").description("The HTTP status code, e.g. `400`"), - fieldWithPath("timestamp").description("The time, in milliseconds, at which the error occurred")))); - } - - @Test - public void indexExample() throws Exception { - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andDo(document("index-example", - links( - linkWithRel("notes").description("The <>"), - linkWithRel("tags").description("The <>"), - linkWithRel("profile").description("The ALPS profile for the service")), - responseFields( - fieldWithPath("_links").description("<> to other resources")))); - - } - - @Test - public void notesListExample() throws Exception { - this.noteRepository.deleteAll(); - - createNote("REST maturity model", - "https://martinfowler.com/articles/richardsonMaturityModel.html"); - createNote("Hypertext Application Language (HAL)", - "https://github.com/mikekelly/hal_specification"); - createNote("Application-Level Profile Semantics (ALPS)", "https://github.com/alps-io/spec"); - - this.mockMvc.perform(get("/notes")) - .andExpect(status().isOk()) - .andDo(document("notes-list-example", - links( - linkWithRel("self").description("Canonical link for this resource"), - linkWithRel("profile").description("The ALPS profile for this resource")), - responseFields( - fieldWithPath("_embedded.notes").description("An array of <>"), - fieldWithPath("_links").description("<> to other resources")))); - } - - @Test - public void notesCreateExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - Map note = new HashMap(); - note.put("title", "REST maturity model"); - note.put("body", "https://martinfowler.com/articles/richardsonMaturityModel.html"); - note.put("tags", Arrays.asList(tagLocation)); - - this.mockMvc.perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))).andExpect( - status().isCreated()) - .andDo(document("notes-create-example", - requestFields( - fieldWithPath("title").description("The title of the note"), - fieldWithPath("body").description("The body of the note"), - fieldWithPath("tags").description("An array of tag resource URIs")))); - } - - @Test - public void noteGetExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - Map note = new HashMap(); - note.put("title", "REST maturity model"); - note.put("body", "https://martinfowler.com/articles/richardsonMaturityModel.html"); - note.put("tags", Arrays.asList(tagLocation)); - - String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - this.mockMvc.perform(get(noteLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(note.get("title")))) - .andExpect(jsonPath("body", is(note.get("body")))) - .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))) - .andDo(print()) - .andDo(document("note-get-example", - links( - linkWithRel("self").description("Canonical link for this <>"), - linkWithRel("note").description("This <>"), - linkWithRel("tags").description("This note's tags")), - responseFields( - fieldWithPath("title").description("The title of the note"), - fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("<> to other resources")))); - } - - @Test - public void tagsListExample() throws Exception { - this.noteRepository.deleteAll(); - this.tagRepository.deleteAll(); - - createTag("REST"); - createTag("Hypermedia"); - createTag("HTTP"); - - this.mockMvc.perform(get("/tags")) - .andExpect(status().isOk()) - .andDo(document("tags-list-example", - links( - linkWithRel("self").description("Canonical link for this resource"), - linkWithRel("profile").description("The ALPS profile for this resource")), - responseFields( - fieldWithPath("_embedded.tags").description("An array of <>"), - fieldWithPath("_links").description("<> to other resources")))); - } - - @Test - public void tagsCreateExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - this.mockMvc.perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andDo(document("tags-create-example", - requestFields( - fieldWithPath("name").description("The name of the tag")))); - } - - @Test - public void noteUpdateExample() throws Exception { - Map note = new HashMap(); - note.put("title", "REST maturity model"); - note.put("body", "https://martinfowler.com/articles/richardsonMaturityModel.html"); - - String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("title", is(note.get("title")))) - .andExpect(jsonPath("body", is(note.get("body")))) - .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))); - - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - Map noteUpdate = new HashMap(); - noteUpdate.put("tags", Arrays.asList(tagLocation)); - - this.mockMvc.perform( - patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(noteUpdate))) - .andExpect(status().isNoContent()) - .andDo(document("note-update-example", - requestFields( - fieldWithPath("title").description("The title of the note").type(JsonFieldType.STRING).optional(), - fieldWithPath("body").description("The body of the note").type(JsonFieldType.STRING).optional(), - fieldWithPath("tags").description("An array of tag resource URIs").optional()))); - } - - @Test - public void tagGetExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - this.mockMvc.perform(get(tagLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("name", is(tag.get("name")))) - .andDo(document("tag-get-example", - links( - linkWithRel("self").description("Canonical link for this <>"), - linkWithRel("tag").description("This <>"), - linkWithRel("notes").description("The <> that have this tag")), - responseFields( - fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("<> to other resources")))); - } - - @Test - public void tagUpdateExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - Map tagUpdate = new HashMap(); - tagUpdate.put("name", "RESTful"); - - this.mockMvc.perform( - patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tagUpdate))) - .andExpect(status().isNoContent()) - .andDo(document("tag-update-example", - requestFields( - fieldWithPath("name").description("The name of the tag")))); - } - - private void createNote(String title, String body) { - Note note = new Note(); - note.setTitle(title); - note.setBody(body); - - this.noteRepository.save(note); - } - - private void createTag(String name) { - Tag tag = new Tag(); - tag.setName(name); - this.tagRepository.save(tag); - } -} diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java deleted file mode 100644 index 45d2b2be1..000000000 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -package com.example.notes; - -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.io.UnsupportedEncodingException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.jayway.jsonpath.JsonPath; - -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class) -@WebAppConfiguration -public class GettingStartedDocumentation { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private WebApplicationContext context; - - private MockMvc mockMvc; - - @Before - public void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(document("{method-name}/{step}/")) - .build(); - } - - @Test - public void index() throws Exception { - this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("_links.notes", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))); - } - - @Test - public void creatingANote() throws JsonProcessingException, Exception { - String noteLocation = createNote(); - MvcResult note = getNote(noteLocation); - - String tagLocation = createTag(); - getTag(tagLocation); - - String taggedNoteLocation = createTaggedNote(tagLocation); - MvcResult taggedNote = getNote(taggedNoteLocation); - getTags(getLink(taggedNote, "tags")); - - tagExistingNote(noteLocation, tagLocation); - getTags(getLink(note, "tags")); - } - - String createNote() throws Exception { - Map note = new HashMap(); - note.put("title", "Note creation with cURL"); - note.put("body", "An example of how to create a note using cURL"); - - String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())) - .andReturn().getResponse().getHeader("Location"); - return noteLocation; - } - - MvcResult getNote(String noteLocation) throws Exception { - return this.mockMvc.perform(get(noteLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))) - .andReturn(); - } - - String createTag() throws Exception, JsonProcessingException { - Map tag = new HashMap(); - tag.put("name", "getting-started"); - - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())) - .andReturn().getResponse().getHeader("Location"); - return tagLocation; - } - - void getTag(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("name", is(notNullValue()))) - .andExpect(jsonPath("_links.notes", is(notNullValue()))); - } - - String createTaggedNote(String tag) throws Exception { - Map note = new HashMap(); - note.put("title", "Tagged note creation with cURL"); - note.put("body", "An example of how to create a tagged note using cURL"); - note.put("tags", Arrays.asList(tag)); - - String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())) - .andReturn().getResponse().getHeader("Location"); - return noteLocation; - } - - void getTags(String noteTagsLocation) throws Exception { - this.mockMvc.perform(get(noteTagsLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))); - } - - void tagExistingNote(String noteLocation, String tagLocation) throws Exception { - Map update = new HashMap(); - update.put("tags", Arrays.asList(tagLocation)); - - this.mockMvc.perform( - patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(update))) - .andExpect(status().isNoContent()); - } - - MvcResult getTaggedExistingNote(String noteLocation) throws Exception { - return this.mockMvc.perform(get(noteLocation)) - .andExpect(status().isOk()) - .andReturn(); - } - - void getTagsForExistingNote(String noteTagsLocation) throws Exception { - this.mockMvc.perform(get(noteTagsLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))); - } - - private String getLink(MvcResult result, String rel) - throws UnsupportedEncodingException { - return JsonPath.parse(result.getResponse().getContentAsString()).read( - "_links." + rel + ".href"); - } -} diff --git a/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs b/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 83412a90a..000000000 --- a/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,295 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.7 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.7 -org.eclipse.jdt.core.formatter.align_type_members_on_columns=false -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_assignment=0 -org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 -org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 -org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 -org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 -org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 -org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 -org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 -org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=1 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1 -org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 -org.eclipse.jdt.core.formatter.blank_lines_before_method=1 -org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 -org.eclipse.jdt.core.formatter.blank_lines_before_package=0 -org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 -org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 -org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false -org.eclipse.jdt.core.formatter.comment.format_block_comments=true -org.eclipse.jdt.core.formatter.comment.format_header=false -org.eclipse.jdt.core.formatter.comment.format_html=true -org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true -org.eclipse.jdt.core.formatter.comment.format_line_comments=true -org.eclipse.jdt.core.formatter.comment.format_source_code=false -org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true -org.eclipse.jdt.core.formatter.comment.indent_root_tags=false -org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=do not insert -org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert -org.eclipse.jdt.core.formatter.comment.line_length=90 -org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true -org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true -org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false -org.eclipse.jdt.core.formatter.compact_else_if=true -org.eclipse.jdt.core.formatter.continuation_indentation=2 -org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 -org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off -org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false -org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true -org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_empty_lines=false -org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true -org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false -org.eclipse.jdt.core.formatter.indentation.size=8 -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert -org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert -org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert -org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.join_lines_in_comments=true -org.eclipse.jdt.core.formatter.join_wrapped_lines=true -org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false -org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false -org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false -org.eclipse.jdt.core.formatter.lineSplit=90 -org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false -org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false -org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 -org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 -org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true -org.eclipse.jdt.core.formatter.tabulation.char=tab -org.eclipse.jdt.core.formatter.tabulation.size=4 -org.eclipse.jdt.core.formatter.use_on_off_tags=false -org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false -org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true -org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true -org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true diff --git a/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs b/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index 1f4325cd5..000000000 --- a/samples/rest-notes-spring-hateoas/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,62 +0,0 @@ -cleanup.add_default_serial_version_id=true -cleanup.add_generated_serial_version_id=false -cleanup.add_missing_annotations=true -cleanup.add_missing_deprecated_annotations=true -cleanup.add_missing_methods=false -cleanup.add_missing_nls_tags=false -cleanup.add_missing_override_annotations=true -cleanup.add_missing_override_annotations_interface_methods=true -cleanup.add_serial_version_id=false -cleanup.always_use_blocks=true -cleanup.always_use_parentheses_in_expressions=false -cleanup.always_use_this_for_non_static_field_access=true -cleanup.always_use_this_for_non_static_method_access=false -cleanup.convert_functional_interfaces=false -cleanup.convert_to_enhanced_for_loop=false -cleanup.correct_indentation=true -cleanup.format_source_code=true -cleanup.format_source_code_changes_only=false -cleanup.insert_inferred_type_arguments=false -cleanup.make_local_variable_final=false -cleanup.make_parameters_final=false -cleanup.make_private_fields_final=false -cleanup.make_type_abstract_if_missing_method=false -cleanup.make_variable_declarations_final=false -cleanup.never_use_blocks=false -cleanup.never_use_parentheses_in_expressions=true -cleanup.organize_imports=true -cleanup.qualify_static_field_accesses_with_declaring_class=false -cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -cleanup.qualify_static_member_accesses_with_declaring_class=true -cleanup.qualify_static_method_accesses_with_declaring_class=false -cleanup.remove_private_constructors=true -cleanup.remove_redundant_type_arguments=true -cleanup.remove_trailing_whitespaces=true -cleanup.remove_trailing_whitespaces_all=true -cleanup.remove_trailing_whitespaces_ignore_empty=false -cleanup.remove_unnecessary_casts=true -cleanup.remove_unnecessary_nls_tags=false -cleanup.remove_unused_imports=true -cleanup.remove_unused_local_variables=false -cleanup.remove_unused_private_fields=true -cleanup.remove_unused_private_members=false -cleanup.remove_unused_private_methods=true -cleanup.remove_unused_private_types=true -cleanup.sort_members=false -cleanup.sort_members_all=false -cleanup.use_anonymous_class_creation=false -cleanup.use_blocks=true -cleanup.use_blocks_only_for_return_and_throw=false -cleanup.use_lambda=true -cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=true -cleanup.use_this_for_non_static_field_access_only_if_necessary=false -cleanup.use_this_for_non_static_method_access=false -cleanup.use_this_for_non_static_method_access_only_if_necessary=true -cleanup.use_type_arguments=false -cleanup_profile=_Spring Rest Docs Cleanup Conventions -cleanup_settings_version=2 -eclipse.preferences.version=1 -formatter_profile=_Spring Rest Docs Java Conventions -formatter_settings_version=12 diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle deleted file mode 100644 index c86d1b274..000000000 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ /dev/null @@ -1,65 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' - } -} - -plugins { - id "org.asciidoctor.convert" version "1.5.3" -} - -apply plugin: 'java' -apply plugin: 'spring-boot' -apply plugin: 'eclipse' - -repositories { - mavenLocal() - maven { url 'https://repo.spring.io/libs-snapshot' } - mavenCentral() -} - -group = 'com.example' - -sourceCompatibility = 1.7 -targetCompatibility = 1.7 - -ext { - snippetsDir = file('build/generated-snippets') -} - -ext['spring-restdocs.version'] = '1.1.4.BUILD-SNAPSHOT' - -dependencies { - compile 'org.springframework.boot:spring-boot-starter-data-jpa' - compile 'org.springframework.boot:spring-boot-starter-hateoas' - - runtime 'com.h2database:h2' - runtime 'org.atteo:evo-inflector:1.2.1' - - testCompile 'com.jayway.jsonpath:json-path' - testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' -} - -test { - outputs.dir snippetsDir -} - -asciidoctor { - attributes 'snippets': snippetsDir - inputs.dir snippetsDir - dependsOn test -} - -jar { - dependsOn asciidoctor - from ("${asciidoctor.outputDir}/html5") { - into 'static/docs' - } -} - -eclipseJdt.onlyIf { false } -cleanEclipseJdt.onlyIf { false } diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 941144813..000000000 Binary files a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 347aa08f3..000000000 --- a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Feb 15 17:16:28 GMT 2016 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/rest-notes-spring-hateoas/gradlew b/samples/rest-notes-spring-hateoas/gradlew deleted file mode 100755 index 9d82f7891..000000000 --- a/samples/rest-notes-spring-hateoas/gradlew +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -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" - which java >/dev/null 2>&1 || 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 - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/rest-notes-spring-hateoas/gradlew.bat b/samples/rest-notes-spring-hateoas/gradlew.bat deleted file mode 100644 index 8a0b282aa..000000000 --- a/samples/rest-notes-spring-hateoas/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@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 - -@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= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -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 init - -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 - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -: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 %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/rest-notes-spring-hateoas/src/docs/asciidoc/api-guide.adoc b/samples/rest-notes-spring-hateoas/src/docs/asciidoc/api-guide.adoc deleted file mode 100644 index 30d3d1591..000000000 --- a/samples/rest-notes-spring-hateoas/src/docs/asciidoc/api-guide.adoc +++ /dev/null @@ -1,315 +0,0 @@ -= RESTful Notes API Guide -Andy Wilkinson; -:doctype: book -:icons: font -:source-highlighter: highlightjs -:toc: left -:toclevels: 4 -:sectlinks: - -[[overview]] -= Overview - -[[overview-http-verbs]] -== HTTP verbs - -RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP verbs. - -|=== -| Verb | Usage - -| `GET` -| Used to retrieve a resource - -| `POST` -| Used to create a new resource - -| `PATCH` -| Used to update an existing resource, including partial updates - -| `DELETE` -| Used to delete an existing resource -|=== - -[[overview-http-status-codes]] -== HTTP status codes - -RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its -use of HTTP status codes. - -|=== -| Status code | Usage - -| `200 OK` -| The request completed successfully - -| `201 Created` -| A new resource has been created successfully. The resource's URI is available from the response's -`Location` header - -| `204 No Content` -| An update to an existing resource has been applied successfully - -| `400 Bad Request` -| The request was malformed. The response body will include an error providing further information - -| `404 Not Found` -| The requested resource did not exist -|=== - -[[overview-headers]] -== Headers - -Every response has the following header(s): - -include::{snippets}/headers-example/response-headers.adoc[] - -[[overview-errors]] -== Errors - -Whenever an error response (status code >= 400) is returned, the body will contain a JSON object -that describes the problem. The error object has the following structure: - -include::{snippets}/error-example/response-fields.adoc[] - -For example, a request that attempts to apply a non-existent tag to a note will produce a -`400 Bad Request` response: - -include::{snippets}/error-example/http-response.adoc[] - -[[overview-hypermedia]] -== Hypermedia - -RESTful Notes uses hypermedia and resources include links to other resources in their -responses. Responses are in https://github.com/mikekelly/hal_specification[Hypertext -Application Language (HAL)] format. Links can be found beneath the `_links` key. Users of -the API should not create URIs themselves, instead they should use the above-described -links to navigate from resource to resource. - -[[resources]] -= Resources - - - -[[resources-index]] -== Index - -The index provides the entry point into the service. - - - -[[resources-index-access]] -=== Accessing the index - -A `GET` request is used to access the index - -==== Response structure - -include::{snippets}/index-example/response-fields.adoc[] - -==== Example response - -include::{snippets}/index-example/http-response.adoc[] - - - -[[resources-index-links]] -==== Links - -include::{snippets}/index-example/links.adoc[] - - - -[[resources-notes]] -== Notes - -The Notes resources is used to create and list notes - - - -[[resources-notes-list]] -=== Listing notes - -A `GET` request will list all of the service's notes. - -==== Response structure - -include::{snippets}/notes-list-example/response-fields.adoc[] - -==== Example request - -include::{snippets}/notes-list-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/notes-list-example/http-response.adoc[] - - - -[[resources-notes-create]] -=== Creating a note - -A `POST` request is used to create a note - -==== Request structure - -include::{snippets}/notes-create-example/request-fields.adoc[] - -==== Example request - -include::{snippets}/notes-create-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/notes-create-example/http-response.adoc[] - - - -[[resources-tags]] -== Tags - -The Tags resource is used to create and list tags. - - - -[[resources-tags-list]] -=== Listing tags - -A `GET` request will list all of the service's tags. - -==== Response structure - -include::{snippets}/tags-list-example/response-fields.adoc[] - -==== Example request - -include::{snippets}/tags-list-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/tags-list-example/http-response.adoc[] - - - -[[resources-tags-create]] -=== Creating a tag - -A `POST` request is used to create a note - -==== Request structure - -include::{snippets}/tags-create-example/request-fields.adoc[] - -==== Example request - -include::{snippets}/tags-create-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/tags-create-example/http-response.adoc[] - - - -[[resources-note]] -== Note - -The Note resource is used to retrieve, update, and delete individual notes - - - -[[resources-note-links]] -=== Links - -include::{snippets}/note-get-example/links.adoc[] - - - -[[resources-note-retrieve]] -=== Retrieve a note - -A `GET` request will retrieve the details of a note - -==== Response structure - -include::{snippets}/note-get-example/response-fields.adoc[] - -==== Example request - -include::{snippets}/note-get-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/note-get-example/http-response.adoc[] - - - -[[resources-note-update]] -=== Update a note - -A `PATCH` request is used to update a note - -==== Request structure - -include::{snippets}/note-update-example/request-fields.adoc[] - -To leave an attribute of a note unchanged, any of the above may be omitted from the request. - -==== Example request - -include::{snippets}/note-update-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/note-update-example/http-response.adoc[] - - -[[resources-tag]] -== Tag - -The Tag resource is used to retrieve, update, and delete individual tags - - - -[[resources-tag-links]] -=== Links - -include::{snippets}/tag-get-example/links.adoc[] - - - -[[resources-tag-retrieve]] -=== Retrieve a tag - -A `GET` request will retrieve the details of a tag - -==== Response structure - -include::{snippets}/tag-get-example/response-fields.adoc[] - -==== Example request - -include::{snippets}/tag-get-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/tag-get-example/http-response.adoc[] - - - -[[resources-tag-update]] -=== Update a tag - -A `PATCH` request is used to update a tag - -==== Request structure - -include::{snippets}/tag-update-example/request-fields.adoc[] - -==== Example request - -include::{snippets}/tag-update-example/curl-request.adoc[] - -==== Example response - -include::{snippets}/tag-update-example/http-response.adoc[] diff --git a/samples/rest-notes-spring-hateoas/src/docs/asciidoc/getting-started-guide.adoc b/samples/rest-notes-spring-hateoas/src/docs/asciidoc/getting-started-guide.adoc deleted file mode 100644 index c951418c2..000000000 --- a/samples/rest-notes-spring-hateoas/src/docs/asciidoc/getting-started-guide.adoc +++ /dev/null @@ -1,176 +0,0 @@ -= RESTful Notes Getting Started Guide -Andy Wilkinson; -:doctype: book -:icons: font -:source-highlighter: highlightjs -:toc: left -:toclevels: 4 -:sectlinks: - -[introduction] -= Introduction - -RESTful Notes is a RESTful web service for creating and storing notes. It uses hypermedia -to describe the relationships between resources and to allow navigation between them. - -[getting-started] -= Getting started - - - -[getting-started-running-the-service] -== Running the service -RESTful Notes is written using https://projects.spring.io/spring-boot[Spring Boot] which -makes it easy to get it up and running so that you can start exploring the REST API. - -The first step is to clone the Git repository: - -[source,bash] ----- -$ git clone https://github.com/spring-projects/spring-restdocs ----- - -Once the clone is complete, you're ready to get the service up and running: - -[source,bash] ----- -$ cd samples/rest-notes-spring-hateoas -$ ./gradlew build -$ java -jar build/libs/*.jar ----- - -You can check that the service is up and running by executing a simple request using -cURL: - -include::{snippets}/index/1/curl-request.adoc[] - -This request should yield the following response: - -include::{snippets}/index/1/http-response.adoc[] - -Note the `_links` in the JSON response. They are key to navigating the API. - - - -[getting-started-creating-a-note] -== Creating a note -Now that you've started the service and verified that it works, the next step is to use -it to create a new note. As you saw above, the URI for working with notes is included as -a link when you perform a `GET` request against the root of the service: - -include::{snippets}/index/1/http-response.adoc[] - -To create a note you need to execute a `POST` request to this URI, including a JSON -payload containing the title and body of the note: - -include::{snippets}/creating-a-note/1/curl-request.adoc[] - -The response from this request should have a status code of `201 Created` and contain a -`Location` header whose value is the URI of the newly created note: - -include::{snippets}/creating-a-note/1/http-response.adoc[] - -To work with the newly created note you use the URI in the `Location` header. For example -you can access the note's details by performing a `GET` request: - -include::{snippets}/creating-a-note/2/curl-request.adoc[] - -This request will produce a response with the note's details in its body: - -include::{snippets}/creating-a-note/2/http-response.adoc[] - -Note the `note-tags` link which we'll make use of later. - - - -[getting-started-creating-a-tag] -== Creating a tag -To make a note easier to find, it can be associated with any number of tags. To be able -to tag a note, you must first create the tag. - -Referring back to the response for the service's index, the URI for working with tags is -include as a link: - -include::{snippets}/index/1/http-response.adoc[] - -To create a tag you need to execute a `POST` request to this URI, including a JSON -payload containing the name of the tag: - -include::{snippets}/creating-a-note/3/curl-request.adoc[] - -The response from this request should have a status code of `201 Created` and contain a -`Location` header whose value is the URI of the newly created tag: - -include::{snippets}/creating-a-note/3/http-response.adoc[] - -To work with the newly created tag you use the URI in the `Location` header. For example -you can access the tag's details by performing a `GET` request: - -include::{snippets}/creating-a-note/4/curl-request.adoc[] - -This request will produce a response with the tag's details in its body: - -include::{snippets}/creating-a-note/4/http-response.adoc[] - - - -[getting-started-tagging-a-note] -== Tagging a note -A tag isn't particularly useful until it's been associated with one or more notes. There -are two ways to tag a note: when the note is first created or by updating an existing -note. We'll look at both of these in turn. - - - -[getting-started-tagging-a-note-creating] -=== Creating a tagged note -The process is largely the same as we saw before, but this time, in addition to providing -a title and body for the note, we'll also provide the tag that we want to be associated -with it. - -Once again we execute a `POST` request, but this time, in an array named tags, we include -the URI of the tag we just created: - -include::{snippets}/creating-a-note/5/curl-request.adoc[] - -Once again, the response's `Location` header tells use the URI of the newly created note: - -include::{snippets}/creating-a-note/5/http-response.adoc[] - -As before, a `GET` request executed against this URI will retrieve the note's details: - -include::{snippets}/creating-a-note/6/curl-request.adoc[] -include::{snippets}/creating-a-note/6/http-response.adoc[] - -To see the note's tags, execute a `GET` request against the URI of the note's -`note-tags` link: - -include::{snippets}/creating-a-note/7/curl-request.adoc[] - -The response shows that, as expected, the note has a single tag: - -include::{snippets}/creating-a-note/7/http-response.adoc[] - - - -[getting-started-tagging-a-note-existing] -=== Tagging an existing note -An existing note can be tagged by executing a `PATCH` request against the note's URI with -a body that contains the array of tags to be associated with the note. We'll use the -URI of the untagged note that we created earlier: - -include::{snippets}/creating-a-note/8/curl-request.adoc[] - -This request should produce a `204 No Content` response: - -include::{snippets}/creating-a-note/8/http-response.adoc[] - -When we first created this note, we noted the `note-tags` link included in its details: - -include::{snippets}/creating-a-note/2/http-response.adoc[] - -We can use that link now and execute a `GET` request to see that the note now has a -single tag: - -include::{snippets}/creating-a-note/9/curl-request.adoc[] -include::{snippets}/creating-a-note/9/http-response.adoc[] diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java deleted file mode 100644 index 158d2319f..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ExceptionSupressingErrorAttributes.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import java.util.Map; - -import org.springframework.boot.autoconfigure.web.DefaultErrorAttributes; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestAttributes; - -@Component -class ExceptionSupressingErrorAttributes extends DefaultErrorAttributes { - - @Override - public Map getErrorAttributes(RequestAttributes requestAttributes, - boolean includeStackTrace) { - Map errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace); - errorAttributes.remove("exception"); - Object message = requestAttributes.getAttribute("javax.servlet.error.message", RequestAttributes.SCOPE_REQUEST); - if (message != null) { - errorAttributes.put("message", message); - } - return errorAttributes; - } -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/IndexController.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/IndexController.java deleted file mode 100644 index bd3e61810..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/IndexController.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; - -import org.springframework.hateoas.ResourceSupport; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/") -public class IndexController { - - @RequestMapping(method=RequestMethod.GET) - public ResourceSupport index() { - ResourceSupport index = new ResourceSupport(); - index.add(linkTo(NotesController.class).withRel("notes")); - index.add(linkTo(TagsController.class).withRel("tags")); - return index; - } - -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NestedContentResource.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NestedContentResource.java deleted file mode 100644 index 4294eed1b..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NestedContentResource.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import org.springframework.hateoas.ResourceSupport; -import org.springframework.hateoas.Resources; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; - -public class NestedContentResource extends ResourceSupport { - - private final Resources nested; - - public NestedContentResource(Iterable toNest) { - this.nested = new Resources(toNest); - } - - @JsonUnwrapped - public Resources getNested() { - return this.nested; - } -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Note.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Note.java deleted file mode 100644 index a4f037ada..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Note.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import java.util.List; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -@Entity -public class Note { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private long id; - - private String title; - - private String body; - - @ManyToMany - private List tags; - - @JsonIgnore - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getBody() { - return body; - } - - public void setBody(String body) { - this.body = body; - } - - @JsonIgnore - public List getTags() { - return tags; - } - - public void setTags(List tags) { - this.tags = tags; - } -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java deleted file mode 100644 index 9afe3b39e..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteInput.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import java.net.URI; -import java.util.Collections; -import java.util.List; - -import org.hibernate.validator.constraints.NotBlank; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class NoteInput { - - @NotBlank - private final String title; - - private final String body; - - private final List tagUris; - - @JsonCreator - public NoteInput(@JsonProperty("title") String title, - @JsonProperty("body") String body, @JsonProperty("tags") List tagUris) { - this.title = title; - this.body = body; - this.tagUris = tagUris == null ? Collections.emptyList() : tagUris; - } - - public String getTitle() { - return title; - } - - public String getBody() { - return body; - } - - @JsonProperty("tags") - public List getTagUris() { - return this.tagUris; - } - -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotePatchInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotePatchInput.java deleted file mode 100644 index b163e673e..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotePatchInput.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import java.net.URI; -import java.util.Collections; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class NotePatchInput { - - @NullOrNotBlank - private final String title; - - private final String body; - - private final List tagUris; - - @JsonCreator - public NotePatchInput(@JsonProperty("title") String title, - @JsonProperty("body") String body, @JsonProperty("tags") List tagUris) { - this.title = title; - this.body = body; - this.tagUris = tagUris == null ? Collections.emptyList() : tagUris; - } - - public String getTitle() { - return title; - } - - public String getBody() { - return body; - } - - @JsonProperty("tags") - public List getTagUris() { - return this.tagUris; - } -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteResourceAssembler.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteResourceAssembler.java deleted file mode 100644 index 4a7f5108e..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteResourceAssembler.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; - -import org.springframework.hateoas.Resource; -import org.springframework.hateoas.mvc.ResourceAssemblerSupport; -import org.springframework.stereotype.Component; - -import com.example.notes.NoteResourceAssembler.NoteResource; - -@Component -public class NoteResourceAssembler extends ResourceAssemblerSupport { - - public NoteResourceAssembler() { - super(NotesController.class, NoteResource.class); - } - - @Override - public NoteResource toResource(Note note) { - NoteResource resource = createResourceWithId(note.getId(), note); - resource.add(linkTo(NotesController.class).slash(note.getId()).slash("tags") - .withRel("note-tags")); - return resource; - } - - @Override - protected NoteResource instantiateResource(Note entity) { - return new NoteResource(entity); - } - - static class NoteResource extends Resource { - - public NoteResource(Note content) { - super(content); - } - } - -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java deleted file mode 100644 index e47020a9a..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NotesController.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -package com.example.notes; - -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; - -import java.net.URI; -import java.util.ArrayList; -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.hateoas.Resource; -import org.springframework.hateoas.ResourceSupport; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.util.UriTemplate; - -import com.example.notes.NoteResourceAssembler.NoteResource; -import com.example.notes.TagResourceAssembler.TagResource; - -@RestController -@RequestMapping("/notes") -public class NotesController { - - private static final UriTemplate TAG_URI_TEMPLATE = new UriTemplate("/tags/{id}"); - - private final NoteRepository noteRepository; - - private final TagRepository tagRepository; - - private final NoteResourceAssembler noteResourceAssembler; - - private final TagResourceAssembler tagResourceAssembler; - - @Autowired - public NotesController(NoteRepository noteRepository, TagRepository tagRepository, - NoteResourceAssembler noteResourceAssembler, - TagResourceAssembler tagResourceAssembler) { - this.noteRepository = noteRepository; - this.tagRepository = tagRepository; - this.noteResourceAssembler = noteResourceAssembler; - this.tagResourceAssembler = tagResourceAssembler; - } - - @RequestMapping(method = RequestMethod.GET) - NestedContentResource all() { - return new NestedContentResource( - this.noteResourceAssembler.toResources(this.noteRepository.findAll())); - } - - @ResponseStatus(HttpStatus.CREATED) - @RequestMapping(method = RequestMethod.POST) - HttpHeaders create(@RequestBody NoteInput noteInput) { - Note note = new Note(); - note.setTitle(noteInput.getTitle()); - note.setBody(noteInput.getBody()); - note.setTags(getTags(noteInput.getTagUris())); - - this.noteRepository.save(note); - - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders - .setLocation(linkTo(NotesController.class).slash(note.getId()).toUri()); - - return httpHeaders; - } - - @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) - void delete(@PathVariable("id") long id) { - this.noteRepository.delete(id); - } - - @RequestMapping(value = "/{id}", method = RequestMethod.GET) - Resource note(@PathVariable("id") long id) { - return this.noteResourceAssembler.toResource(findNoteById(id)); - } - - @RequestMapping(value = "/{id}/tags", method = RequestMethod.GET) - ResourceSupport noteTags(@PathVariable("id") long id) { - return new NestedContentResource( - this.tagResourceAssembler.toResources(findNoteById(id).getTags())); - } - - @RequestMapping(value = "/{id}", method = RequestMethod.PATCH) - @ResponseStatus(HttpStatus.NO_CONTENT) - void updateNote(@PathVariable("id") long id, @RequestBody NotePatchInput noteInput) { - Note note = findNoteById(id); - if (noteInput.getTagUris() != null) { - note.setTags(getTags(noteInput.getTagUris())); - } - if (noteInput.getTitle() != null) { - note.setTitle(noteInput.getTitle()); - } - if (noteInput.getBody() != null) { - note.setBody(noteInput.getBody()); - } - this.noteRepository.save(note); - } - - private Note findNoteById(long id) { - Note note = this.noteRepository.findById(id); - if (note == null) { - throw new ResourceDoesNotExistException(); - } - return note; - } - - private List getTags(List tagLocations) { - List tags = new ArrayList<>(tagLocations.size()); - for (URI tagLocation: tagLocations) { - Tag tag = this.tagRepository.findById(extractTagId(tagLocation)); - if (tag == null) { - throw new IllegalArgumentException("The tag '" + tagLocation - + "' does not exist"); - } - tags.add(tag); - } - return tags; - } - - private long extractTagId(URI tagLocation) { - try { - String idString = TAG_URI_TEMPLATE.match(tagLocation.toASCIIString()).get( - "id"); - return Long.valueOf(idString); - } - catch (RuntimeException ex) { - throw new IllegalArgumentException("The tag '" + tagLocation + "' is invalid"); - } - } -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesControllerAdvice.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesControllerAdvice.java deleted file mode 100644 index e621709e5..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesControllerAdvice.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import java.io.IOException; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; - -@ControllerAdvice -public class RestNotesControllerAdvice { - - @ExceptionHandler(IllegalArgumentException.class) - public void handleIllegalArgumentException(IllegalArgumentException ex, - HttpServletResponse response) throws IOException { - response.sendError(HttpStatus.BAD_REQUEST.value(), ex.getMessage()); - } - - @ExceptionHandler(ResourceDoesNotExistException.class) - public void handleResourceDoesNotExistException(ResourceDoesNotExistException ex, - HttpServletRequest request, HttpServletResponse response) throws IOException { - response.sendError(HttpStatus.NOT_FOUND.value(), - "The resource '" + request.getRequestURI() + "' does not exist"); - } - -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java deleted file mode 100644 index f08144656..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example.notes; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class RestNotesSpringHateoas { - - public static void main(String[] args) { - SpringApplication.run(RestNotesSpringHateoas.class, args); - } - -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Tag.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Tag.java deleted file mode 100644 index f721fcb50..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/Tag.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import java.util.List; - -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.ManyToMany; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -@Entity -public class Tag { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private long id; - - private String name; - - @ManyToMany(mappedBy = "tags") - private List notes; - - @JsonIgnore - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @JsonIgnore - public List getNotes() { - return notes; - } - - public void setNotes(List notes) { - this.notes = notes; - } -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagPatchInput.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagPatchInput.java deleted file mode 100644 index b392492a5..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagPatchInput.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class TagPatchInput { - - @NullOrNotBlank - private final String name; - - @JsonCreator - public TagPatchInput(@NullOrNotBlank @JsonProperty("name") String name) { - this.name = name; - } - - public String getName() { - return name; - } - -} \ No newline at end of file diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepository.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepository.java deleted file mode 100644 index 6bc9fc3cb..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -package com.example.notes; - -import org.springframework.data.repository.CrudRepository; - -public interface TagRepository extends CrudRepository { - - Tag findById(long id); - -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagResourceAssembler.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagResourceAssembler.java deleted file mode 100644 index 45d881799..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagResourceAssembler.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2014 the original author or 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. - */ - -package com.example.notes; - -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; - -import org.springframework.hateoas.Resource; -import org.springframework.hateoas.mvc.ResourceAssemblerSupport; -import org.springframework.stereotype.Component; - -import com.example.notes.TagResourceAssembler.TagResource; - -@Component -public class TagResourceAssembler extends ResourceAssemblerSupport { - - public TagResourceAssembler() { - super(TagsController.class, TagResource.class); - } - - @Override - public TagResource toResource(Tag tag) { - TagResource resource = createResourceWithId(tag.getId(), tag); - resource.add(linkTo(TagsController.class).slash(tag.getId()).slash("notes") - .withRel("tagged-notes")); - return resource; - } - - @Override - protected TagResource instantiateResource(Tag entity) { - return new TagResource(entity); - } - - static class TagResource extends Resource { - - public TagResource(Tag content) { - super(content); - } - } - -} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java deleted file mode 100644 index fcf09f73f..000000000 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagsController.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -package com.example.notes; - -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.hateoas.Resource; -import org.springframework.hateoas.ResourceSupport; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -import com.example.notes.NoteResourceAssembler.NoteResource; -import com.example.notes.TagResourceAssembler.TagResource; - -@RestController -@RequestMapping("tags") -public class TagsController { - - private final TagRepository repository; - - private final NoteResourceAssembler noteResourceAssembler; - - private final TagResourceAssembler tagResourceAssembler; - - @Autowired - public TagsController(TagRepository repository, - NoteResourceAssembler noteResourceAssembler, - TagResourceAssembler tagResourceAssembler) { - this.repository = repository; - this.noteResourceAssembler = noteResourceAssembler; - this.tagResourceAssembler = tagResourceAssembler; - } - - @RequestMapping(method = RequestMethod.GET) - NestedContentResource all() { - return new NestedContentResource( - this.tagResourceAssembler.toResources(this.repository.findAll())); - } - - @ResponseStatus(HttpStatus.CREATED) - @RequestMapping(method = RequestMethod.POST) - HttpHeaders create(@RequestBody TagInput tagInput) { - Tag tag = new Tag(); - tag.setName(tagInput.getName()); - - this.repository.save(tag); - - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setLocation(linkTo(TagsController.class).slash(tag.getId()).toUri()); - - return httpHeaders; - } - - @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) - void delete(@PathVariable("id") long id) { - this.repository.delete(id); - } - - @RequestMapping(value = "/{id}", method = RequestMethod.GET) - Resource tag(@PathVariable("id") long id) { - Tag tag = findTagById(id); - return this.tagResourceAssembler.toResource(tag); - } - - @RequestMapping(value = "/{id}/notes", method = RequestMethod.GET) - ResourceSupport tagNotes(@PathVariable("id") long id) { - Tag tag = findTagById(id); - return new NestedContentResource( - this.noteResourceAssembler.toResources(tag.getNotes())); - } - - private Tag findTagById(long id) { - Tag tag = this.repository.findById(id); - if (tag == null) { - throw new ResourceDoesNotExistException(); - } - return tag; - } - - @RequestMapping(value = "/{id}", method = RequestMethod.PATCH) - @ResponseStatus(HttpStatus.NO_CONTENT) - void updateTag(@PathVariable("id") long id, @RequestBody TagPatchInput tagInput) { - Tag tag = findTagById(id); - if (tagInput.getName() != null) { - tag.setName(tagInput.getName()); - } - this.repository.save(tag); - } -} diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java deleted file mode 100644 index 23d51fcf4..000000000 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Copyright 2014-2019 the original author or 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. - */ - -package com.example.notes; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.RequestDispatcher; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.constraints.ConstraintDescriptions; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.restdocs.payload.FieldDescriptor; -import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.util.StringUtils; -import org.springframework.web.context.WebApplicationContext; - -import com.fasterxml.jackson.databind.ObjectMapper; - -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSpringHateoas.class) -@WebAppConfiguration -public class ApiDocumentation { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); - - private RestDocumentationResultHandler documentationHandler; - - @Autowired - private NoteRepository noteRepository; - - @Autowired - private TagRepository tagRepository; - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private WebApplicationContext context; - - private MockMvc mockMvc; - - @Before - public void setUp() { - this.documentationHandler = document("{method-name}", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint())); - - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(this.documentationHandler) - .build(); - } - - @Test - public void headersExample() throws Exception { - this.mockMvc - .perform(get("/")) - .andExpect(status().isOk()) - .andDo(this.documentationHandler.document( - responseHeaders( - headerWithName("Content-Type").description("The Content-Type of the payload, e.g. `application/hal+json`")))); - } - - @Test - public void errorExample() throws Exception { - this.mockMvc - .perform(get("/error") - .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) - .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, "/notes") - .requestAttr(RequestDispatcher.ERROR_MESSAGE, "The tag 'http://localhost:8080/tags/123' does not exist")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("error", is("Bad Request"))) - .andExpect(jsonPath("timestamp", is(notNullValue()))) - .andExpect(jsonPath("status", is(400))) - .andExpect(jsonPath("path", is(notNullValue()))) - .andDo(this.documentationHandler.document( - responseFields( - fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"), - fieldWithPath("message").description("A description of the cause of the error"), - fieldWithPath("path").description("The path to which the request was made"), - fieldWithPath("status").description("The HTTP status code, e.g. `400`"), - fieldWithPath("timestamp").description("The time, in milliseconds, at which the error occurred")))); - } - - @Test - public void indexExample() throws Exception { - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andDo(this.documentationHandler.document( - links( - linkWithRel("notes").description("The <>"), - linkWithRel("tags").description("The <>")), - responseFields( - fieldWithPath("_links").description("<> to other resources")))); - } - - @Test - public void notesListExample() throws Exception { - this.noteRepository.deleteAll(); - - createNote("REST maturity model", "https://martinfowler.com/articles/richardsonMaturityModel.html"); - createNote("Hypertext Application Language (HAL)", "https://github.com/mikekelly/hal_specification"); - createNote("Application-Level Profile Semantics (ALPS)", "https://github.com/alps-io/spec"); - - this.mockMvc - .perform(get("/notes")) - .andExpect(status().isOk()) - .andDo(this.documentationHandler.document( - responseFields( - fieldWithPath("_embedded.notes").description("An array of <>")))); - } - - @Test - public void notesCreateExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform(post("/tags") - .contentType(MediaTypes.HAL_JSON) - .content(this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andReturn().getResponse().getHeader("Location"); - - Map note = new HashMap(); - note.put("title", "REST maturity model"); - note.put("body", "https://martinfowler.com/articles/richardsonMaturityModel.html"); - note.put("tags", Arrays.asList(tagLocation)); - - ConstrainedFields fields = new ConstrainedFields(NoteInput.class); - - this.mockMvc - .perform(post("/notes") - .contentType(MediaTypes.HAL_JSON) - .content(this.objectMapper.writeValueAsString(note))) - .andExpect( - status().isCreated()) - .andDo(this.documentationHandler.document( - requestFields( - fields.withPath("title").description("The title of the note"), - fields.withPath("body").description("The body of the note"), - fields.withPath("tags").description("An array of tag resource URIs")))); - } - - @Test - public void noteGetExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform(post("/tags") - .contentType(MediaTypes.HAL_JSON) - .content(this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andReturn().getResponse().getHeader("Location"); - - Map note = new HashMap(); - note.put("title", "REST maturity model"); - note.put("body", "https://martinfowler.com/articles/richardsonMaturityModel.html"); - note.put("tags", Arrays.asList(tagLocation)); - - String noteLocation = this.mockMvc - .perform(post("/notes") - .contentType(MediaTypes.HAL_JSON) - .content(this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andReturn().getResponse().getHeader("Location"); - - this.mockMvc - .perform(get(noteLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(note.get("title")))) - .andExpect(jsonPath("body", is(note.get("body")))) - .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue()))) - .andDo(this.documentationHandler.document( - links( - linkWithRel("self").description("This <>"), - linkWithRel("note-tags").description("This note's <>")), - responseFields( - fieldWithPath("title").description("The title of the note"), - fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("<> to other resources")))); - - } - - @Test - public void tagsListExample() throws Exception { - this.noteRepository.deleteAll(); - this.tagRepository.deleteAll(); - - createTag("REST"); - createTag("Hypermedia"); - createTag("HTTP"); - - this.mockMvc - .perform(get("/tags")) - .andExpect(status().isOk()) - .andDo(this.documentationHandler.document( - responseFields( - fieldWithPath("_embedded.tags").description("An array of <>")))); - } - - @Test - public void tagsCreateExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - ConstrainedFields fields = new ConstrainedFields(TagInput.class); - - this.mockMvc - .perform(post("/tags") - .contentType(MediaTypes.HAL_JSON) - .content(this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andDo(this.documentationHandler.document( - requestFields( - fields.withPath("name").description("The name of the tag")))); - } - - @Test - public void noteUpdateExample() throws Exception { - Map note = new HashMap(); - note.put("title", "REST maturity model"); - note.put("body", "https://martinfowler.com/articles/richardsonMaturityModel.html"); - - String noteLocation = this.mockMvc - .perform(post("/notes") - .contentType(MediaTypes.HAL_JSON) - .content(this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andReturn().getResponse().getHeader("Location"); - - this.mockMvc - .perform(get(noteLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(note.get("title")))) - .andExpect(jsonPath("body", is(note.get("body")))) - .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue()))); - - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform(post("/tags") - .contentType(MediaTypes.HAL_JSON) - .content(this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andReturn().getResponse().getHeader("Location"); - - Map noteUpdate = new HashMap(); - noteUpdate.put("tags", Arrays.asList(tagLocation)); - - ConstrainedFields fields = new ConstrainedFields(NotePatchInput.class); - - this.mockMvc - .perform(patch(noteLocation) - .contentType(MediaTypes.HAL_JSON) - .content(this.objectMapper.writeValueAsString(noteUpdate))) - .andExpect(status().isNoContent()) - .andDo(this.documentationHandler.document( - requestFields( - fields.withPath("title") - .description("The title of the note") - .type(JsonFieldType.STRING) - .optional(), - fields.withPath("body") - .description("The body of the note") - .type(JsonFieldType.STRING) - .optional(), - fields.withPath("tags") - .description("An array of tag resource URIs")))); - } - - @Test - public void tagGetExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform(post("/tags") - .contentType(MediaTypes.HAL_JSON) - .content(this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andReturn().getResponse().getHeader("Location"); - - this.mockMvc - .perform(get(tagLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("name", is(tag.get("name")))) - .andDo(this.documentationHandler.document( - links( - linkWithRel("self").description("This <>"), - linkWithRel("tagged-notes").description("The <> that have this tag")), - responseFields( - fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("<> to other resources")))); - } - - @Test - public void tagUpdateExample() throws Exception { - Map tag = new HashMap(); - tag.put("name", "REST"); - - String tagLocation = this.mockMvc - .perform(post("/tags") - .contentType(MediaTypes.HAL_JSON) - .content(this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andReturn().getResponse().getHeader("Location"); - - Map tagUpdate = new HashMap(); - tagUpdate.put("name", "RESTful"); - - ConstrainedFields fields = new ConstrainedFields(TagPatchInput.class); - - this.mockMvc - .perform(patch(tagLocation) - .contentType(MediaTypes.HAL_JSON) - .content(this.objectMapper.writeValueAsString(tagUpdate))) - .andExpect(status().isNoContent()) - .andDo(this.documentationHandler.document( - requestFields( - fields.withPath("name").description("The name of the tag")))); - } - - private void createNote(String title, String body) { - Note note = new Note(); - note.setTitle(title); - note.setBody(body); - - this.noteRepository.save(note); - } - - private void createTag(String name) { - Tag tag = new Tag(); - tag.setName(name); - this.tagRepository.save(tag); - } - - private static class ConstrainedFields { - - private final ConstraintDescriptions constraintDescriptions; - - ConstrainedFields(Class input) { - this.constraintDescriptions = new ConstraintDescriptions(input); - } - - private FieldDescriptor withPath(String path) { - return fieldWithPath(path).attributes(key("constraints").value(StringUtils - .collectionToDelimitedString(this.constraintDescriptions - .descriptionsForProperty(path), ". "))); - } - } - -} diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java deleted file mode 100644 index e14c2ce91..000000000 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -package com.example.notes; - -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.io.UnsupportedEncodingException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.jayway.jsonpath.JsonPath; - -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSpringHateoas.class) -@WebAppConfiguration -public class GettingStartedDocumentation { - - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private WebApplicationContext context; - - private MockMvc mockMvc; - - @Before - public void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(document("{method-name}/{step}/", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()))) - .build(); - } - - @Test - public void index() throws Exception { - this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("_links.notes", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))); - } - - @Test - public void creatingANote() throws JsonProcessingException, Exception { - String noteLocation = createNote(); - MvcResult note = getNote(noteLocation); - - String tagLocation = createTag(); - getTag(tagLocation); - - String taggedNoteLocation = createTaggedNote(tagLocation); - MvcResult taggedNote = getNote(taggedNoteLocation); - getTags(getLink(taggedNote, "note-tags")); - - tagExistingNote(noteLocation, tagLocation); - getTags(getLink(note, "note-tags")); - } - - String createNote() throws Exception { - Map note = new HashMap(); - note.put("title", "Note creation with cURL"); - note.put("body", "An example of how to create a note using cURL"); - - String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())) - .andReturn().getResponse().getHeader("Location"); - return noteLocation; - } - - MvcResult getNote(String noteLocation) throws Exception { - return this.mockMvc.perform(get(noteLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue()))) - .andReturn(); - } - - String createTag() throws Exception, JsonProcessingException { - Map tag = new HashMap(); - tag.put("name", "getting-started"); - - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())) - .andReturn().getResponse().getHeader("Location"); - return tagLocation; - } - - void getTag(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("name", is(notNullValue()))) - .andExpect(jsonPath("_links.tagged-notes", is(notNullValue()))); - } - - String createTaggedNote(String tag) throws Exception { - Map note = new HashMap(); - note.put("title", "Tagged note creation with cURL"); - note.put("body", "An example of how to create a tagged note using cURL"); - note.put("tags", Arrays.asList(tag)); - - String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())) - .andReturn().getResponse().getHeader("Location"); - return noteLocation; - } - - void getTags(String noteTagsLocation) throws Exception { - this.mockMvc.perform(get(noteTagsLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))); - } - - void tagExistingNote(String noteLocation, String tagLocation) throws Exception { - Map update = new HashMap(); - update.put("tags", Arrays.asList(tagLocation)); - - this.mockMvc.perform( - patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(update))) - .andExpect(status().isNoContent()); - } - - MvcResult getTaggedExistingNote(String noteLocation) throws Exception { - return this.mockMvc.perform(get(noteLocation)) - .andExpect(status().isOk()) - .andReturn(); - } - - void getTagsForExistingNote(String noteTagsLocation) throws Exception { - this.mockMvc.perform(get(noteTagsLocation)) - .andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))); - } - - private String getLink(MvcResult result, String rel) - throws UnsupportedEncodingException { - return JsonPath.parse(result.getResponse().getContentAsString()).read( - "_links." + rel + ".href"); - } -} diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java deleted file mode 100644 index b4f2f5977..000000000 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/NullOrNotBlankTests.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -package com.example.notes; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -import java.util.Set; - -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; - -import org.junit.Test; - - -public class NullOrNotBlankTests { - - private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); - - @Test - public void nullValue() { - Set> violations = validator.validate(new Constrained(null)); - assertThat(violations.size(), is(0)); - } - - @Test - public void zeroLengthValue() { - Set> violations = validator.validate(new Constrained("")); - assertThat(violations.size(), is(2)); - } - - @Test - public void blankValue() { - Set> violations = validator.validate(new Constrained(" ")); - assertThat(violations.size(), is(2)); - } - - @Test - public void nonBlankValue() { - Set> violations = validator.validate(new Constrained("test")); - assertThat(violations.size(), is(0)); - } - - static class Constrained { - - @NullOrNotBlank - private final String value; - - public Constrained(String value) { - this.value = value; - } - - } - -} diff --git a/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties b/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties deleted file mode 100644 index 433d946d5..000000000 --- a/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties +++ /dev/null @@ -1,2 +0,0 @@ -com.example.notes.NullOrNotBlank.description=Must be null or not blank -org.hibernate.validator.constraints.NotBlank.description=Must not be blank \ No newline at end of file diff --git a/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet b/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet deleted file mode 100644 index cd1e825c8..000000000 --- a/samples/rest-notes-spring-hateoas/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet +++ /dev/null @@ -1,11 +0,0 @@ -|=== -|Path|Type|Description|Constraints - -{{#fields}} -|{{path}} -|{{type}} -|{{description}} -|{{constraints}} - -{{/fields}} -|=== \ No newline at end of file diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle deleted file mode 100644 index 3a0307b8b..000000000 --- a/samples/testng/build.gradle +++ /dev/null @@ -1,62 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' - } -} - -plugins { - id "org.asciidoctor.convert" version "1.5.3" -} - -apply plugin: 'java' -apply plugin: 'spring-boot' -apply plugin: 'eclipse' - -repositories { - mavenLocal() - maven { url 'https://repo.spring.io/libs-snapshot' } - mavenCentral() -} - -group = 'com.example' - -sourceCompatibility = 1.7 -targetCompatibility = 1.7 - -ext { - snippetsDir = file('build/generated-snippets') -} -ext['spring-restdocs.version'] = '1.1.4.BUILD-SNAPSHOT' - -dependencies { - compile 'org.springframework.boot:spring-boot-starter-web' - testCompile('org.springframework:spring-test') { - exclude group: 'junit', module: 'junit;' - } - testCompile 'org.testng:testng:6.9.10' - testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' -} - -test { - useTestNG() - outputs.dir snippetsDir -} - -asciidoctor { - attributes 'snippets': snippetsDir - inputs.dir snippetsDir - dependsOn test -} - -jar { - dependsOn asciidoctor - from ("${asciidoctor.outputDir}/html5") { - into 'static/docs' - } -} - -eclipseJdt.onlyIf { false } -cleanEclipseJdt.onlyIf { false } diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.jar b/samples/testng/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 941144813..000000000 Binary files a/samples/testng/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.properties b/samples/testng/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9332fb8f6..000000000 --- a/samples/testng/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Feb 15 17:16:46 GMT 2016 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/testng/gradlew b/samples/testng/gradlew deleted file mode 100755 index 9d82f7891..000000000 --- a/samples/testng/gradlew +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -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" - which java >/dev/null 2>&1 || 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 - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/testng/gradlew.bat b/samples/testng/gradlew.bat deleted file mode 100644 index 8a0b282aa..000000000 --- a/samples/testng/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@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 - -@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= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -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 init - -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 - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -: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 %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/samples/testng/src/docs/asciidoc/index.adoc b/samples/testng/src/docs/asciidoc/index.adoc deleted file mode 100644 index ea8b47e49..000000000 --- a/samples/testng/src/docs/asciidoc/index.adoc +++ /dev/null @@ -1,22 +0,0 @@ -= Spring REST Docs TestNG Sample -Andy Wilkinson; -:doctype: book -:icons: font -:source-highlighter: highlightjs - -Sample application demonstrating how to use Spring REST Docs with TestNG. - -`SampleTestNgApplicationTests` makes a call to a very simple service and produces three -documentation snippets. - -One showing how to make a request using cURL: - -include::{snippets}/sample/curl-request.adoc[] - -One showing the HTTP request: - -include::{snippets}/sample/http-request.adoc[] - -And one showing the HTTP response: - -include::{snippets}/sample/http-response.adoc[] \ No newline at end of file diff --git a/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java b/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java deleted file mode 100644 index f0c6ab91a..000000000 --- a/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example.testng; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@SpringBootApplication -public class SampleTestNgApplication { - - public static void main(String[] args) { - new SpringApplication(SampleTestNgApplication.class).run(args); - } - - @RestController - private static class SampleController { - - @RequestMapping("/") - public String index() { - return "Hello, World"; - } - - } - -} diff --git a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java deleted file mode 100644 index a665c0873..000000000 --- a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package com.example.testng; - -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.lang.reflect.Method; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.restdocs.ManualRestDocumentation; -import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -@SpringApplicationConfiguration(classes=SampleTestNgApplication.class) -@WebAppConfiguration -public class SampleTestNgApplicationTests extends AbstractTestNGSpringContextTests { - - private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation("build/generated-snippets"); - - @Autowired - private WebApplicationContext context; - - private MockMvc mockMvc; - - @BeforeMethod - public void setUp(Method method) { - this.mockMvc = MockMvcBuilders.webAppContextSetup(context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - this.restDocumentation.beforeTest(getClass(), method.getName()); - } - - @AfterMethod - public void tearDown() { - this.restDocumentation.afterTest(); - } - - @Test - public void sample() throws Exception { - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andDo(document("sample")); - } - -} diff --git a/settings.gradle b/settings.gradle index 8d6619a19..fd285ac1a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,29 @@ -rootProject.name = 'spring-restdocs' +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + maven { url = 'https://repo.spring.io/snapshot' } + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "io.spring.javaformat") { + useModule "io.spring.javaformat:spring-javaformat-gradle-plugin:${requested.version}" + } + } + } +} -include 'docs' -include 'spring-restdocs-core' -include 'spring-restdocs-mockmvc' -include 'spring-restdocs-restassured' \ No newline at end of file +plugins { + id "io.spring.develocity.conventions" version "0.0.23" +} + +rootProject.name = "spring-restdocs" + +include "docs" +include "spring-restdocs-asciidoctor" +include "spring-restdocs-bom" +include "spring-restdocs-core" +include "spring-restdocs-mockmvc" +include "spring-restdocs-platform" +include "spring-restdocs-restassured" +include "spring-restdocs-webtestclient" diff --git a/spring-restdocs-asciidoctor/build.gradle b/spring-restdocs-asciidoctor/build.gradle new file mode 100644 index 000000000..f9d1f685d --- /dev/null +++ b/spring-restdocs-asciidoctor/build.gradle @@ -0,0 +1,24 @@ +plugins { + id "java-library" + id "maven-publish" +} + +description = "Spring REST Docs Asciidoctor Extension" + +dependencies { + implementation("org.asciidoctor:asciidoctorj") + + internal(platform(project(":spring-restdocs-platform"))) + + testImplementation("org.apache.pdfbox:pdfbox") + testImplementation("org.assertj:assertj-core") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.springframework:spring-core") + + testRuntimeOnly("org.asciidoctor:asciidoctorj-pdf") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.named("test") { + useJUnitPlatform() +} diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java new file mode 100644 index 000000000..7fa6ce5df --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.asciidoctor; + +import org.asciidoctor.ast.Document; +import org.asciidoctor.extension.Preprocessor; +import org.asciidoctor.extension.PreprocessorReader; +import org.asciidoctor.extension.Reader; + +/** + * {@link Preprocessor} that sets defaults for REST Docs-related {@link Document} + * attributes. + * + * @author Andy Wilkinson + */ +final class DefaultAttributesPreprocessor extends Preprocessor { + + private final SnippetsDirectoryResolver snippetsDirectoryResolver = new SnippetsDirectoryResolver(); + + @Override + public Reader process(Document document, PreprocessorReader reader) { + document.setAttribute("snippets", this.snippetsDirectoryResolver.getSnippetsDirectory(document.getAttributes()), + false); + return reader; + } + +} diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java new file mode 100644 index 000000000..0a58a7cf9 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package org.springframework.restdocs.asciidoctor; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.jruby.extension.spi.ExtensionRegistry; + +/** + * {@link ExtensionRegistry} for Spring REST Docs. + * + * @author Andy Wilkinson + */ +public final class RestDocsExtensionRegistry implements ExtensionRegistry { + + @Override + public void register(Asciidoctor asciidoctor) { + asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesPreprocessor()); + asciidoctor.rubyExtensionRegistry() + .loadClass(RestDocsExtensionRegistry.class.getResourceAsStream("/extensions/operation_block_macro.rb")) + .blockMacro("operation", "OperationBlockMacro"); + } + +} diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java new file mode 100644 index 000000000..dfbb957a8 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java @@ -0,0 +1,79 @@ +/* + * Copyright 2014-2021 the original author or 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. + */ + +package org.springframework.restdocs.asciidoctor; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Resolves the directory from which snippets can be read for inclusion in an Asciidoctor + * document. The resolved directory is absolute. + * + * @author Andy Wilkinson + */ +class SnippetsDirectoryResolver { + + File getSnippetsDirectory(Map attributes) { + if (System.getProperty("maven.home") != null) { + return getMavenSnippetsDirectory(attributes); + } + return getGradleSnippetsDirectory(attributes); + } + + private File getMavenSnippetsDirectory(Map attributes) { + Path docdir = Paths.get(getRequiredAttribute(attributes, "docdir")); + return new File(findPom(docdir).getParent().toFile(), "target/generated-snippets").getAbsoluteFile(); + } + + private Path findPom(Path docdir) { + Path path = docdir; + while (path != null) { + Path pom = path.resolve("pom.xml"); + if (Files.isRegularFile(pom)) { + return pom; + } + path = path.getParent(); + } + throw new IllegalStateException("pom.xml not found in '" + docdir + "' or above"); + } + + private File getGradleSnippetsDirectory(Map attributes) { + return new File(getRequiredAttribute(attributes, "gradle-projectdir", + () -> getRequiredAttribute(attributes, "projectdir")), "build/generated-snippets") + .getAbsoluteFile(); + } + + private String getRequiredAttribute(Map attributes, String name) { + return getRequiredAttribute(attributes, name, null); + } + + private String getRequiredAttribute(Map attributes, String name, Supplier fallback) { + String attribute = (String) attributes.get(name); + if (attribute == null || attribute.length() == 0) { + if (fallback != null) { + return fallback.get(); + } + throw new IllegalStateException(name + " attribute not found"); + } + return attribute; + } + +} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ResourceDoesNotExistException.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/package-info.java similarity index 76% rename from samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ResourceDoesNotExistException.java rename to spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/package-info.java index 03e23e746..fd03600b7 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/ResourceDoesNotExistException.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,7 @@ * limitations under the License. */ -package com.example.notes; - -@SuppressWarnings("serial") -public class ResourceDoesNotExistException extends RuntimeException { - -} +/** + * Support for Asciidoctor. + */ +package org.springframework.restdocs.asciidoctor; diff --git a/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.jruby.extension.spi.ExtensionRegistry b/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.jruby.extension.spi.ExtensionRegistry new file mode 100644 index 000000000..7123d5b51 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.jruby.extension.spi.ExtensionRegistry @@ -0,0 +1 @@ +org.springframework.restdocs.asciidoctor.RestDocsExtensionRegistry diff --git a/spring-restdocs-asciidoctor/src/main/resources/extensions/operation_block_macro.rb b/spring-restdocs-asciidoctor/src/main/resources/extensions/operation_block_macro.rb new file mode 100644 index 000000000..66feb5f5e --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/resources/extensions/operation_block_macro.rb @@ -0,0 +1,156 @@ +require 'asciidoctor/extensions' +require 'stringio' +require 'asciidoctor/logging' + +# Spring REST Docs block macro to import multiple snippet of an operation at +# once +# +# Usage +# +# operation::operation-name[snippets='snippet-name1,snippet-name2'] +# +class OperationBlockMacro < Asciidoctor::Extensions::BlockMacroProcessor + use_dsl + named :operation + include Asciidoctor::Logging + + def process(parent, operation, attributes) + snippets_dir = parent.document.attributes['snippets'].to_s + snippet_names = attributes.fetch 'snippets', '' + operation = parent.sub_attributes operation + snippet_titles = SnippetTitles.new parent.document.attributes + content = read_snippets(snippets_dir, snippet_names, parent, operation, + snippet_titles) + add_blocks(content, parent.document, parent) unless content.empty? + nil + end + + def read_snippets(snippets_dir, snippet_names, parent, operation, + snippet_titles) + snippets = snippets_to_include(snippet_names, snippets_dir, operation) + if snippets.empty? + location = parent.document.reader.cursor_at_mark + logger.warn message_with_context "No snippets were found for operation #{operation} in "\ + "#{snippets_dir}", source_location: location + "No snippets found for operation::#{operation}" + else + do_read_snippets(snippets, parent, operation, snippet_titles) + end + end + + def do_read_snippets(snippets, parent, operation, snippet_titles) + content = StringIO.new + content.set_encoding "UTF-8" + section_id = parent.id + snippets.each do |snippet| + append_snippet_block(content, snippet, section_id, + operation, snippet_titles, parent) + end + content.string + end + + def add_blocks(content, doc, parent) + options = { safe: doc.options[:safe], attributes: doc.attributes.clone } + options[:attributes].delete 'leveloffset' + fragment = Asciidoctor.load content, options + # use a template to get the correct sectname and level for blocks to append + template = create_section(parent, '', {}) + fragment.blocks.each do |b| + b.parent = parent + # might be a standard block and no section in case of 'No snippets were found for operation' + if b.respond_to?(:sectname) + b.sectname = template.sectname + end + b.level = template.level + parent << b + end + parent.find_by.each do |b| + b.parent = b.parent unless b.is_a? Asciidoctor::Document + end + end + + def snippets_to_include(snippet_names, snippets_dir, operation) + if snippet_names.empty? + all_snippets snippets_dir, operation + else + snippet_names.split(',').map do |name| + path = File.join snippets_dir, operation, "#{name}.adoc" + Snippet.new path, name + end + end + end + + def all_snippets(snippets_dir, operation) + operation_dir = File.join snippets_dir, operation + return [] unless Dir.exist? operation_dir + Dir.entries(operation_dir) + .sort + .select { |file| file.end_with? '.adoc' } + .map { |file| Snippet.new(File.join(operation_dir, file), file[0..-6]) } + end + + def append_snippet_block(content, snippet, section_id, + operation, snippet_titles, parent) + write_title content, snippet, section_id, snippet_titles + write_content content, snippet, operation, parent + end + + def write_content(content, snippet, operation, parent) + if File.file? snippet.path + content.puts File.readlines(snippet.path, :encoding => 'UTF-8').join + else + location = parent.document.reader.cursor_at_mark + logger.warn message_with_context "Snippet #{snippet.name} not found at #{snippet.path} for"\ + " operation #{operation}", source_location: location + content.puts "Snippet #{snippet.name} not found for"\ + " operation::#{operation}" + content.puts '' + end + end + + def write_title(content, snippet, id, snippet_titles) + section_level = '==' + title = snippet_titles.title_for_snippet snippet + content.puts "[[#{id}_#{snippet.name.sub '-', '_'}]]" + content.puts "#{section_level} #{title}" + content.puts '' + end + + # Details of a snippet to be rendered + class Snippet + attr_reader :name, :path + + def initialize(path, name) + @path = path + @name = name + @snippet_titles + end + end + + class SnippetTitles + @defaults = { 'http-request' => 'HTTP request', + 'curl-request' => 'Curl request', + 'httpie-request' => 'HTTPie request', + 'request-body' => 'Request body', + 'request-fields' => 'Request fields', + 'http-response' => 'HTTP response', + 'response-body' => 'Response body', + 'response-fields' => 'Response fields', + 'links' => 'Links' } + + class << self + attr_reader :defaults + end + + def initialize(document_attributes) + @document_attributes = document_attributes + end + + def title_for_snippet(snippet) + attribute_name = "operation-#{snippet.name}-title" + @document_attributes.fetch attribute_name do + SnippetTitles.defaults.fetch snippet.name, snippet.name.sub('-', ' ').capitalize + end + end + end +end diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/AbstractOperationBlockMacroTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/AbstractOperationBlockMacroTests.java new file mode 100644 index 000000000..d6df5f917 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/AbstractOperationBlockMacroTests.java @@ -0,0 +1,264 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.asciidoctor; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import org.apache.pdfbox.contentstream.PDFStreamEngine; +import org.apache.pdfbox.contentstream.operator.Operator; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSString; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.Attributes; +import org.asciidoctor.Options; +import org.asciidoctor.SafeMode; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.util.FileSystemUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Base class for tests for the Ruby operation block macro. + * + * @author Gerrit Meier + * @author Andy Wilkinson + */ +abstract class AbstractOperationBlockMacroTests { + + private final Asciidoctor asciidoctor = Asciidoctor.Factory.create(); + + @TempDir + protected File temp; + + private Options options; + + @BeforeEach + void setUp() throws IOException { + prepareOperationSnippets(getBuildOutputLocation()); + this.options = Options.builder().safe(SafeMode.UNSAFE).baseDir(getSourceLocation()).build(); + this.options.setAttributes(getAttributes()); + CapturingLogHandler.clear(); + } + + @AfterEach + void verifyLogging() { + assertThat(CapturingLogHandler.getLogRecords()).isEmpty(); + } + + private void prepareOperationSnippets(File buildOutputLocation) throws IOException { + File destination = new File(buildOutputLocation, "generated-snippets/some-operation"); + destination.mkdirs(); + FileSystemUtils.copyRecursively(new File("src/test/resources/some-operation"), destination); + } + + @Test + void codeBlockSnippetInclude() throws Exception { + String result = this.asciidoctor.convert("operation::some-operation[snippets='curl-request']", this.options); + assertThat(result).isEqualTo(getExpectedContentFromFile("snippet-simple")); + } + + @Test + void operationWithParameterizedName() throws Exception { + Attributes attributes = getAttributes(); + attributes.setAttribute("name", "some"); + this.options.setAttributes(attributes); + String result = this.asciidoctor.convert("operation::{name}-operation[snippets='curl-request']", this.options); + assertThat(result).isEqualTo(getExpectedContentFromFile("snippet-simple")); + } + + @Test + void codeBlockSnippetIncludeWithPdfBackend() throws Exception { + File output = configurePdfOutput(); + this.asciidoctor.convert("operation::some-operation[snippets='curl-request']", this.options); + assertThat(extractStrings(output)).containsExactly("Curl request", "$ curl 'http://localhost:8080/' -i", "1"); + } + + @Test + void tableSnippetInclude() throws Exception { + String result = this.asciidoctor.convert("operation::some-operation[snippets='response-fields']", this.options); + assertThat(result).isEqualTo(getExpectedContentFromFile("snippet-table")); + } + + @Test + void tableSnippetIncludeWithPdfBackend() throws Exception { + File output = configurePdfOutput(); + this.asciidoctor.convert("operation::some-operation[snippets='response-fields']", this.options); + assertThat(extractStrings(output)).containsExactly("Response fields", "Path", "Type", "Description", "a", + "Object", "one", "a.b", "Number", "two", "a.c", "String", "three", "1"); + } + + @Test + void includeSnippetInSection() throws Exception { + String result = this.asciidoctor.convert("= A\n:doctype: book\n:sectnums:\n\nAlpha\n\n== B\n\nBravo\n\n" + + "operation::some-operation[snippets='curl-request']\n\n== C\n", this.options); + assertThat(result).isEqualTo(getExpectedContentFromFile("snippet-in-section")); + } + + @Test + void includeSnippetInSectionWithAbsoluteLevelOffset() throws Exception { + String result = this.asciidoctor + .convert("= A\n:doctype: book\n:sectnums:\n:leveloffset: 1\n\nAlpha\n\n= B\n\nBravo\n\n" + + "operation::some-operation[snippets='curl-request']\n\n= C\n", this.options); + assertThat(result).isEqualTo(getExpectedContentFromFile("snippet-in-section")); + } + + @Test + void includeSnippetInSectionWithRelativeLevelOffset() throws Exception { + String result = this.asciidoctor + .convert("= A\n:doctype: book\n:sectnums:\n:leveloffset: +1\n\nAlpha\n\n= B\n\nBravo\n\n" + + "operation::some-operation[snippets='curl-request']\n\n= C\n", this.options); + assertThat(result).isEqualTo(getExpectedContentFromFile("snippet-in-section")); + } + + @Test + void includeSnippetInSectionWithPdfBackend() throws Exception { + File output = configurePdfOutput(); + this.asciidoctor.convert("== Section\n" + "operation::some-operation[snippets='curl-request']", this.options); + assertThat(extractStrings(output)).containsExactly("Section", "Curl request", + "$ curl 'http://localhost:8080/' -i", "1"); + } + + @Test + void includeMultipleSnippets() throws Exception { + String result = this.asciidoctor.convert("operation::some-operation[snippets='curl-request,http-request']", + this.options); + assertThat(result).isEqualTo(getExpectedContentFromFile("multiple-snippets")); + } + + @Test + void useMacroWithoutSnippetAttributeAddsAllSnippets() throws Exception { + String result = this.asciidoctor.convert("operation::some-operation[]", this.options); + assertThat(result).isEqualTo(getExpectedContentFromFile("all-snippets")); + } + + @Test + void useMacroWithEmptySnippetAttributeAddsAllSnippets() throws Exception { + String result = this.asciidoctor.convert("operation::some-operation[snippets=]", this.options); + assertThat(result).isEqualTo(getExpectedContentFromFile("all-snippets")); + } + + @Test + void includingMissingSnippetAddsWarning() throws Exception { + String result = this.asciidoctor.convert("operation::some-operation[snippets='missing-snippet']", this.options); + assertThat(result).startsWith(getExpectedContentFromFile("missing-snippet")); + assertThat(CapturingLogHandler.getLogRecords()).hasSize(1); + assertThat(CapturingLogHandler.getLogRecords().get(0).getMessage()) + .contains("Snippet missing-snippet not found"); + assertThat(CapturingLogHandler.getLogRecords().get(0).getCursor().getLineNumber()).isEqualTo(1); + CapturingLogHandler.getLogRecords().clear(); + } + + @Test + void defaultTitleIsProvidedForCustomSnippet() throws Exception { + String result = this.asciidoctor.convert("operation::some-operation[snippets='custom-snippet']", this.options); + assertThat(result).isEqualTo(getExpectedContentFromFile("custom-snippet-default-title")); + } + + @Test + void missingOperationIsHandledGracefully() throws Exception { + String result = this.asciidoctor.convert("operation::missing-operation[]", this.options); + assertThat(result).startsWith(getExpectedContentFromFile("missing-operation")); + assertThat(CapturingLogHandler.getLogRecords()).hasSize(1); + assertThat(CapturingLogHandler.getLogRecords().get(0).getMessage()) + .contains("No snippets were found for operation missing-operation"); + assertThat(CapturingLogHandler.getLogRecords().get(0).getCursor().getLineNumber()).isEqualTo(1); + CapturingLogHandler.getLogRecords().clear(); + } + + @Test + void titleOfBuiltInSnippetCanBeCustomizedUsingDocumentAttribute() throws URISyntaxException, IOException { + String result = this.asciidoctor.convert(":operation-curl-request-title: Example request\n" + + "operation::some-operation[snippets='curl-request']", this.options); + assertThat(result).isEqualTo(getExpectedContentFromFile("built-in-snippet-custom-title")); + } + + @Test + void titleOfCustomSnippetCanBeCustomizedUsingDocumentAttribute() throws Exception { + String result = this.asciidoctor.convert(":operation-custom-snippet-title: Customized title\n" + + "operation::some-operation[snippets='custom-snippet']", this.options); + assertThat(result).isEqualTo(getExpectedContentFromFile("custom-snippet-custom-title")); + } + + private String getExpectedContentFromFile(String fileName) throws URISyntaxException, IOException { + Path filePath = Paths.get(this.getClass().getResource("/operations/" + fileName + ".html").toURI()); + String content = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8); + if (isWindows()) { + return content.replace("\r\n", "\n"); + } + return content; + } + + private boolean isWindows() { + return File.separatorChar == '\\'; + } + + protected abstract Attributes getAttributes(); + + protected abstract File getBuildOutputLocation(); + + protected abstract File getSourceLocation(); + + private File configurePdfOutput() { + this.options.setBackend("pdf"); + File output = new File("build/output.pdf"); + this.options.setToFile(output.getAbsolutePath()); + return output; + } + + private List extractStrings(File pdfFile) throws IOException { + PDDocument pdf = PDDocument.load(pdfFile); + assertThat(pdf.getNumberOfPages()).isEqualTo(1); + StringExtractor stringExtractor = new StringExtractor(); + stringExtractor.processPage(pdf.getPage(0)); + return stringExtractor.getStrings(); + } + + private static final class StringExtractor extends PDFStreamEngine { + + private final List strings = new ArrayList<>(); + + @Override + protected void processOperator(Operator operator, List operands) throws IOException { + if ("Tj".equals(operator.getName())) { + for (COSBase operand : operands) { + if (operand instanceof COSString) { + this.strings.add((((COSString) operand).getASCII())); + } + } + } + } + + private List getStrings() { + return this.strings; + } + + } + +} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/CapturingLogHandler.java similarity index 51% rename from samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java rename to spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/CapturingLogHandler.java index 37bb3012a..f60ce09c4 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/TagInput.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/CapturingLogHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,25 +14,29 @@ * limitations under the License. */ -package com.example.notes; +package org.springframework.restdocs.asciidoctor; -import org.hibernate.validator.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.asciidoctor.log.LogHandler; +import org.asciidoctor.log.LogRecord; -public class TagInput { +public class CapturingLogHandler implements LogHandler { - @NotBlank - private final String name; + private static final List logRecords = new ArrayList(); - @JsonCreator - public TagInput(@NotBlank @JsonProperty("name") String name) { - this.name = name; + @Override + public void log(LogRecord logRecord) { + logRecords.add(logRecord); } - public String getName() { - return name; + static List getLogRecords() { + return logRecords; + } + + static void clear() { + logRecords.clear(); } } diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java new file mode 100644 index 000000000..b5921dbaa --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.asciidoctor; + +import java.io.File; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.Attributes; +import org.asciidoctor.Options; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultAttributesPreprocessor}. + * + * @author Andy Wilkinson + */ +class DefaultAttributesPreprocessorTests { + + @Test + void snippetsAttributeIsSet() { + String converted = createAsciidoctor().convert("{snippets}", createOptions("projectdir=../../..")); + assertThat(converted).contains("build" + File.separatorChar + "generated-snippets"); + } + + @Test + void snippetsAttributeFromConvertArgumentIsNotOverridden() { + String converted = createAsciidoctor().convert("{snippets}", + createOptions("snippets=custom projectdir=../../..")); + assertThat(converted).contains("custom"); + } + + @Test + void snippetsAttributeFromDocumentPreambleIsNotOverridden() { + String converted = createAsciidoctor().convert(":snippets: custom\n{snippets}", + createOptions("projectdir=../../..")); + assertThat(converted).contains("custom"); + } + + private Options createOptions(String attributes) { + Options options = Options.builder().build(); + options.setAttributes(Attributes.builder().arguments(attributes).build()); + return options; + } + + private Asciidoctor createAsciidoctor() { + Asciidoctor asciidoctor = Asciidoctor.Factory.create(); + asciidoctor.javaExtensionRegistry().preprocessor(new DefaultAttributesPreprocessor()); + return asciidoctor; + } + +} diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/GradleOperationBlockMacroTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/GradleOperationBlockMacroTests.java new file mode 100644 index 000000000..1ed21adb9 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/GradleOperationBlockMacroTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.asciidoctor; + +import java.io.File; + +import org.asciidoctor.Attributes; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Tests for Ruby operation block macro when used in a Gradle build. + * + * @author Andy Wilkinson + */ +@ParameterizedClass +@ValueSource(strings = { "projectdir", "gradle-projectdir" }) +class GradleOperationBlockMacroTests extends AbstractOperationBlockMacroTests { + + private final String attributeName; + + GradleOperationBlockMacroTests(String attributeName) { + this.attributeName = attributeName; + } + + @Override + protected Attributes getAttributes() { + Attributes attributes = Attributes.builder() + .attribute(this.attributeName, new File(this.temp, "gradle-project").getAbsolutePath()) + .build(); + return attributes; + } + + @Override + protected File getBuildOutputLocation() { + File outputLocation = new File(this.temp, "gradle-project/build"); + outputLocation.mkdirs(); + return outputLocation; + } + + @Override + protected File getSourceLocation() { + File sourceLocation = new File(this.temp, "gradle-project/src/docs/asciidoc"); + if (!sourceLocation.exists()) { + sourceLocation.mkdirs(); + } + return sourceLocation; + } + +} diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/MavenOperationBlockMacroTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/MavenOperationBlockMacroTests.java new file mode 100644 index 000000000..f02f326f9 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/MavenOperationBlockMacroTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.asciidoctor; + +import java.io.File; +import java.io.IOException; + +import org.asciidoctor.Attributes; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +/** + * Tests for Ruby operation block macro when used in a Maven build. + * + * @author Andy Wilkinson + */ +class MavenOperationBlockMacroTests extends AbstractOperationBlockMacroTests { + + @BeforeEach + void setMavenHome() { + System.setProperty("maven.home", "maven-home"); + } + + @AfterEach + void clearMavenHome() { + System.clearProperty("maven.home"); + } + + @Override + protected Attributes getAttributes() { + try { + File sourceLocation = getSourceLocation(); + new File(sourceLocation.getParentFile().getParentFile().getParentFile(), "pom.xml").createNewFile(); + Attributes attributes = Attributes.builder().attribute("docdir", sourceLocation.getAbsolutePath()).build(); + return attributes; + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + @Override + protected File getBuildOutputLocation() { + File outputLocation = new File(this.temp, "maven-project/target"); + outputLocation.mkdirs(); + return outputLocation; + } + + @Override + protected File getSourceLocation() { + File sourceLocation = new File(this.temp, "maven-project/src/main/asciidoc"); + if (!sourceLocation.exists()) { + sourceLocation.mkdirs(); + } + return sourceLocation; + } + +} diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java new file mode 100644 index 000000000..9ca213b93 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.asciidoctor; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link SnippetsDirectoryResolver}. + * + * @author Andy Wilkinson + */ +public class SnippetsDirectoryResolverTests { + + @TempDir + File temp; + + @Test + public void mavenProjectsUseTargetGeneratedSnippets() throws IOException { + new File(this.temp, "pom.xml").createNewFile(); + Map attributes = new HashMap<>(); + attributes.put("docdir", new File(this.temp, "src/main/asciidoc").getAbsolutePath()); + File snippetsDirectory = getMavenSnippetsDirectory(attributes); + assertThat(snippetsDirectory).isAbsolute(); + assertThat(snippetsDirectory).isEqualTo(new File(this.temp, "target/generated-snippets")); + } + + @Test + public void illegalStateExceptionWhenMavenPomCannotBeFound() { + Map attributes = new HashMap<>(); + String docdir = new File(this.temp, "src/main/asciidoc").getAbsolutePath(); + attributes.put("docdir", docdir); + assertThatIllegalStateException().isThrownBy(() -> getMavenSnippetsDirectory(attributes)) + .withMessage("pom.xml not found in '" + docdir + "' or above"); + } + + @Test + public void illegalStateWhenDocdirAttributeIsNotSetInMavenProject() { + Map attributes = new HashMap<>(); + assertThatIllegalStateException().isThrownBy(() -> getMavenSnippetsDirectory(attributes)) + .withMessage("docdir attribute not found"); + } + + @Test + public void gradleProjectsUseBuildGeneratedSnippetsBeneathGradleProjectdir() { + Map attributes = new HashMap<>(); + attributes.put("gradle-projectdir", "project/dir"); + File snippetsDirectory = new SnippetsDirectoryResolver().getSnippetsDirectory(attributes); + assertThat(snippetsDirectory).isAbsolute(); + assertThat(snippetsDirectory).isEqualTo(new File("project/dir/build/generated-snippets").getAbsoluteFile()); + } + + @Test + public void gradleProjectsUseBuildGeneratedSnippetsBeneathGradleProjectdirWhenBothItAndProjectdirAreSet() { + Map attributes = new HashMap<>(); + attributes.put("gradle-projectdir", "project/dir"); + attributes.put("projectdir", "fallback/dir"); + File snippetsDirectory = new SnippetsDirectoryResolver().getSnippetsDirectory(attributes); + assertThat(snippetsDirectory).isAbsolute(); + assertThat(snippetsDirectory).isEqualTo(new File("project/dir/build/generated-snippets").getAbsoluteFile()); + } + + @Test + public void gradleProjectsUseBuildGeneratedSnippetsBeneathProjectdirWhenGradleProjectdirIsNotSet() { + Map attributes = new HashMap<>(); + attributes.put("projectdir", "project/dir"); + File snippetsDirectory = new SnippetsDirectoryResolver().getSnippetsDirectory(attributes); + assertThat(snippetsDirectory).isAbsolute(); + assertThat(snippetsDirectory).isEqualTo(new File("project/dir/build/generated-snippets").getAbsoluteFile()); + } + + @Test + public void illegalStateWhenGradleProjectdirAndProjectdirAttributesAreNotSetInGradleProject() { + Map attributes = new HashMap<>(); + assertThatIllegalStateException() + .isThrownBy(() -> new SnippetsDirectoryResolver().getSnippetsDirectory(attributes)) + .withMessage("projectdir attribute not found"); + } + + private File getMavenSnippetsDirectory(Map attributes) { + System.setProperty("maven.home", "/maven/home"); + try { + return new SnippetsDirectoryResolver().getSnippetsDirectory(attributes); + } + finally { + System.clearProperty("maven.home"); + } + } + +} diff --git a/spring-restdocs-asciidoctor/src/test/resources/META-INF/services/org.asciidoctor.log.LogHandler b/spring-restdocs-asciidoctor/src/test/resources/META-INF/services/org.asciidoctor.log.LogHandler new file mode 100644 index 000000000..28aea3ae1 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/META-INF/services/org.asciidoctor.log.LogHandler @@ -0,0 +1 @@ +org.springframework.restdocs.asciidoctor.CapturingLogHandler \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/all-snippets.html b/spring-restdocs-asciidoctor/src/test/resources/operations/all-snippets.html new file mode 100644 index 000000000..508e45984 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/all-snippets.html @@ -0,0 +1,67 @@ +
          +

          Curl request

          +
          +
          +
          +
          $ curl 'http://localhost:8080/' -i
          +
          +
          +
          +
          +
          +

          Custom snippet

          +
          +
          +
          +
          mycustomsnippet-äöü
          +
          +
          +
          +
          +
          +

          HTTP request

          +
          +
          +
          +
          GET / HTTP/1.1
          +Host: localhost:8080
          +
          +
          +
          +
          +
          +

          Response fields

          +
          + +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
          PathTypeDescription

          a

          Object

          one

          a.b

          Number

          two

          a.c

          String

          three

          +
          +
          \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/built-in-snippet-custom-title.html b/spring-restdocs-asciidoctor/src/test/resources/operations/built-in-snippet-custom-title.html new file mode 100644 index 000000000..a75533b3f --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/built-in-snippet-custom-title.html @@ -0,0 +1,10 @@ +
          +

          Example request

          +
          +
          +
          +
          $ curl 'http://localhost:8080/' -i
          +
          +
          +
          +
          \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/custom-snippet-custom-title.html b/spring-restdocs-asciidoctor/src/test/resources/operations/custom-snippet-custom-title.html new file mode 100644 index 000000000..0fed5ed2a --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/custom-snippet-custom-title.html @@ -0,0 +1,10 @@ +
          +

          Customized title

          +
          +
          +
          +
          mycustomsnippet-äöü
          +
          +
          +
          +
          \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/custom-snippet-default-title.html b/spring-restdocs-asciidoctor/src/test/resources/operations/custom-snippet-default-title.html new file mode 100644 index 000000000..f5613259f --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/custom-snippet-default-title.html @@ -0,0 +1,10 @@ +
          +

          Custom snippet

          +
          +
          +
          +
          mycustomsnippet-äöü
          +
          +
          +
          +
          \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/missing-operation.html b/spring-restdocs-asciidoctor/src/test/resources/operations/missing-operation.html new file mode 100644 index 000000000..3c4cbbc83 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/missing-operation.html @@ -0,0 +1,3 @@ +
          +

          No snippets found for operation::missing-operation

          +
          \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/missing-snippet.html b/spring-restdocs-asciidoctor/src/test/resources/operations/missing-snippet.html new file mode 100644 index 000000000..70bf737ca --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/missing-snippet.html @@ -0,0 +1,8 @@ +
          +

          Missing snippet

          +
          +
          +

          Snippet missing-snippet not found for operation::some-operation

          +
          +
          +
          \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/multiple-snippets.html b/spring-restdocs-asciidoctor/src/test/resources/operations/multiple-snippets.html new file mode 100644 index 000000000..89b8ffba5 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/multiple-snippets.html @@ -0,0 +1,21 @@ +
          +

          Curl request

          +
          +
          +
          +
          $ curl 'http://localhost:8080/' -i
          +
          +
          +
          +
          +
          +

          HTTP request

          +
          +
          +
          +
          GET / HTTP/1.1
          +Host: localhost:8080
          +
          +
          +
          +
          \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-in-section.html b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-in-section.html new file mode 100644 index 000000000..a8ec3f471 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-in-section.html @@ -0,0 +1,29 @@ +
          +
          +
          +

          Alpha

          +
          +
          +
          +
          +

          1. B

          +
          +
          +

          Bravo

          +
          +
          +

          1.1. Curl request

          +
          +
          +
          $ curl 'http://localhost:8080/' -i
          +
          +
          +
          +
          +
          +
          +

          2. C

          +
          + +
          +
          \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-simple.html b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-simple.html new file mode 100644 index 000000000..35a3fa548 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-simple.html @@ -0,0 +1,10 @@ +
          +

          Curl request

          +
          +
          +
          +
          $ curl 'http://localhost:8080/' -i
          +
          +
          +
          +
          \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-table.html b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-table.html new file mode 100644 index 000000000..b01bc522a --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-table.html @@ -0,0 +1,36 @@ +
          +

          Response fields

          +
          + +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
          PathTypeDescription

          a

          Object

          one

          a.b

          Number

          two

          a.c

          String

          three

          +
          +
          \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-with-level.html b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-with-level.html new file mode 100644 index 000000000..727ccf024 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/operations/snippet-with-level.html @@ -0,0 +1,8 @@ +
          +

          Curl request

          +
          +
          +
          $ curl 'http://localhost:8080/' -i
          +
          +
          +
          \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/sample-snippet.adoc b/spring-restdocs-asciidoctor/src/test/resources/sample-snippet.adoc new file mode 100644 index 000000000..0a3d1fa7e --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/sample-snippet.adoc @@ -0,0 +1,4 @@ +[source,bash] +---- +$ curl 'http://localhost:8080/' -i -H 'Accept: application/hal+json' +---- \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/some-operation/curl-request.adoc b/spring-restdocs-asciidoctor/src/test/resources/some-operation/curl-request.adoc new file mode 100644 index 000000000..0183405fd --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/some-operation/curl-request.adoc @@ -0,0 +1,4 @@ +[source,bash] +---- +$ curl 'http://localhost:8080/' -i +---- \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/some-operation/custom-snippet.adoc b/spring-restdocs-asciidoctor/src/test/resources/some-operation/custom-snippet.adoc new file mode 100644 index 000000000..7933144b1 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/some-operation/custom-snippet.adoc @@ -0,0 +1,4 @@ +[source,http,options="nowrap"] +---- +mycustomsnippet-äöü +---- \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/some-operation/http-request.adoc b/spring-restdocs-asciidoctor/src/test/resources/some-operation/http-request.adoc new file mode 100644 index 000000000..2034fb60a --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/some-operation/http-request.adoc @@ -0,0 +1,6 @@ +[source,http,options="nowrap"] +---- +GET / HTTP/1.1 +Host: localhost:8080 + +---- \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/resources/some-operation/response-fields.adoc b/spring-restdocs-asciidoctor/src/test/resources/some-operation/response-fields.adoc new file mode 100644 index 000000000..9c0dcd213 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/some-operation/response-fields.adoc @@ -0,0 +1,16 @@ +|=== +|Path|Type|Description + +|`a` +|`Object` +|one + +|`a.b` +|`Number` +|two + +|`a.c` +|`String` +|three + +|=== \ No newline at end of file diff --git a/spring-restdocs-bom/build.gradle b/spring-restdocs-bom/build.gradle new file mode 100644 index 000000000..bd0b4ce5c --- /dev/null +++ b/spring-restdocs-bom/build.gradle @@ -0,0 +1,16 @@ +plugins { + id "java-platform" + id "maven-publish" +} + +description = "Spring REST Docs Bill of Materials" + +dependencies { + constraints { + api(project(":spring-restdocs-asciidoctor")) + api(project(":spring-restdocs-core")) + api(project(":spring-restdocs-mockmvc")) + api(project(":spring-restdocs-restassured")) + api(project(":spring-restdocs-webtestclient")) + } +} \ No newline at end of file diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 4a8417072..496bb1a39 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -1,4 +1,11 @@ -description = 'Spring REST Docs Core' +plugins { + id "java-library" + id "java-test-fixtures" + id "maven-publish" + id "optional-dependencies" +} + +description = "Spring REST Docs Core" configurations { jarjar @@ -7,57 +14,83 @@ configurations { } task jmustacheRepackJar(type: Jar) { repackJar -> - repackJar.baseName = "restdocs-jmustache-repack" - repackJar.version = dependencyManagement.managedVersions['com.samskivert:jmustache'] + repackJar.archiveBaseName = "restdocs-jmustache-repack" + repackJar.archiveVersion = jmustacheVersion doLast() { - project.ant { + ant { taskdef name: "jarjar", classname: "com.tonicsystems.jarjar.JarJarTask", classpath: configurations.jarjar.asPath - jarjar(destfile: repackJar.archivePath) { + jarjar(destfile: repackJar.archiveFile.get()) { configurations.jmustache.each { originalJar -> - zipfileset(src: originalJar, includes: '**/*.class') + zipfileset(src: originalJar, includes: "**/*.class") } - rule(pattern: 'com.samskivert.**', result: 'org.springframework.restdocs.@1') + rule(pattern: "com.samskivert.**", result: "org.springframework.restdocs.@1") } } } } dependencies { - compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'org.springframework:spring-web' - compile 'javax.servlet:javax.servlet-api' - compile files(jmustacheRepackJar) - jarjar 'com.googlecode.jarjar:jarjar:1.3' - jmustache 'com.samskivert:jmustache@jar' - optional 'commons-codec:commons-codec' - optional 'javax.validation:validation-api' - optional 'junit:junit' - optional 'org.hibernate:hibernate-validator' - testCompile 'org.mockito:mockito-core' - testCompile 'org.hamcrest:hamcrest-core' - testCompile 'org.hamcrest:hamcrest-library' - testCompile 'org.springframework:spring-test' - testRuntime 'org.glassfish:javax.el:3.0.0' + compileOnly("org.apiguardian:apiguardian-api") + + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("org.springframework:spring-web") + implementation(files(jmustacheRepackJar)) + + internal(platform(project(":spring-restdocs-platform"))) + + jarjar("com.googlecode.jarjar:jarjar:1.3") + + jmustache(platform(project(":spring-restdocs-platform"))) + jmustache("com.samskivert:jmustache@jar") + + optional(platform(project(":spring-restdocs-platform"))) + optional("jakarta.validation:jakarta.validation-api") + optional("org.hibernate.validator:hibernate-validator") + optional("org.junit.jupiter:junit-jupiter-api") + + testFixturesApi(platform(project(":spring-restdocs-platform"))) + testFixturesApi("org.assertj:assertj-core") + testFixturesApi("org.junit.jupiter:junit-jupiter") + testFixturesApi("org.mockito:mockito-core") + + testFixturesCompileOnly("org.apiguardian:apiguardian-api") + + testFixturesImplementation(files(jmustacheRepackJar)) + testFixturesImplementation("org.hamcrest:hamcrest-library") + testFixturesImplementation("org.mockito:mockito-core") + testFixturesImplementation("org.springframework:spring-core") + testFixturesImplementation("org.springframework:spring-web") + + testFixturesRuntimeOnly("org.junit.platform:junit-platform-launcher") + + testCompileOnly("org.apiguardian:apiguardian-api") + + testImplementation("org.assertj:assertj-core") + testImplementation("org.javamoney:moneta") + testImplementation("org.mockito:mockito-core") + testImplementation("org.springframework:spring-test") + + testRuntimeOnly("org.apache.tomcat.embed:tomcat-embed-el") + testRuntimeOnly("org.junit.platform:junit-platform-engine") } jar { dependsOn jmustacheRepackJar - from(zipTree(jmustacheRepackJar.archivePath)) { + from(zipTree(jmustacheRepackJar.archiveFile.get())) { include "org/springframework/restdocs/**" } } -task testJar(type: Jar) { - classifier "test" - from sourceSets.test.output +components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { + skip() } -artifacts { - testArtifacts testJar +components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { + skip() } -test { - jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*,excludes=org.springframework.restdocs.mustache.*" -} \ No newline at end of file +tasks.named("test") { + useJUnitPlatform() +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java deleted file mode 100644 index cca32006e..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs; - -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -/** - * A JUnit {@link TestRule} used to automatically manage the - * {@link RestDocumentationContext}. - * - * @author Andy Wilkinson - * @since 1.1.0 - */ -public class JUnitRestDocumentation - implements RestDocumentationContextProvider, TestRule { - - private final ManualRestDocumentation delegate; - - /** - * Creates a new {@code JUnitRestDocumentation} instance that will generate snippets - * to the given {@code outputDirectory}. - * - * @param outputDirectory the output directory - */ - public JUnitRestDocumentation(String outputDirectory) { - this.delegate = new ManualRestDocumentation(outputDirectory); - } - - @Override - public Statement apply(final Statement base, final Description description) { - return new Statement() { - - @Override - public void evaluate() throws Throwable { - Class testClass = description.getTestClass(); - String methodName = description.getMethodName(); - JUnitRestDocumentation.this.delegate.beforeTest(testClass, methodName); - try { - base.evaluate(); - } - finally { - JUnitRestDocumentation.this.delegate.afterTest(); - } - } - - }; - - } - - @Override - public RestDocumentationContext beforeOperation() { - return this.delegate.beforeOperation(); - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java index bb10a6863..037189541 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,15 @@ import java.io.File; +import org.junit.jupiter.api.extension.Extension; + /** * {@code ManualRestDocumentation} is used to manually manage the * {@link RestDocumentationContext}. Primarly intended for use with TestNG, but suitable * for use in any environment where manual management of the context is required. *

          - * Users of JUnit should use {@link JUnitRestDocumentation} and take advantage of its - * Rule-based support for automatic management of the context. + * Users of JUnit should use {@link RestDocumentationExtension} and take advantage of its + * {@link Extension}-based support for automatic management of the context. * * @author Andy Wilkinson * @since 1.1.0 @@ -33,16 +35,27 @@ public final class ManualRestDocumentation implements RestDocumentationContextPr private final File outputDirectory; - private RestDocumentationContext context; + private StandardRestDocumentationContext context; + + /** + * Creates a new {@code ManualRestDocumentation} instance that will generate snippets + * to <gradle/maven build path>/generated-snippets. + */ + public ManualRestDocumentation() { + this(getDefaultOutputDirectory()); + } /** * Creates a new {@code ManualRestDocumentation} instance that will generate snippets * to the given {@code outputDirectory}. - * * @param outputDirectory the output directory */ public ManualRestDocumentation(String outputDirectory) { - this.outputDirectory = new File(outputDirectory); + this(new File(outputDirectory)); + } + + private ManualRestDocumentation(File outputDirectory) { + this.outputDirectory = outputDirectory; } /** @@ -50,19 +63,15 @@ public ManualRestDocumentation(String outputDirectory) { * {@link RestDocumentationContext} for the test on the given {@code testClass} with * the given {@code testMethodName}. Must be followed by a call to * {@link #afterTest()} once the test has completed. - * * @param testClass the test class * @param testMethodName the name of the test method * @throws IllegalStateException if a context has already be created */ - @SuppressWarnings("deprecation") public void beforeTest(Class testClass, String testMethodName) { if (this.context != null) { - throw new IllegalStateException( - "Context already exists. Did you forget to call afterTest()?"); + throw new IllegalStateException("Context already exists. Did you forget to call afterTest()?"); } - this.context = new RestDocumentationContext(testClass, testMethodName, - this.outputDirectory); + this.context = new StandardRestDocumentationContext(testClass, testMethodName, this.outputDirectory); } /** @@ -79,4 +88,11 @@ public RestDocumentationContext beforeOperation() { return this.context; } + private static File getDefaultOutputDirectory() { + if (new File("pom.xml").exists()) { + return new File("target/generated-snippets"); + } + return new File("build/generated-snippets"); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java deleted file mode 100644 index 0e9334d7e..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -package org.springframework.restdocs; - -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -/** - * A JUnit {@link TestRule} used to bootstrap the generation of REST documentation from - * JUnit tests. - * - * @author Andy Wilkinson - * @deprecated Since 1.1 in favor of {@link JUnitRestDocumentation} - */ -@Deprecated -public class RestDocumentation implements TestRule, RestDocumentationContextProvider { - - private final JUnitRestDocumentation delegate; - - /** - * Creates a new {@code RestDocumentation} instance that will generate snippets to the - * given {@code outputDirectory}. - * - * @param outputDirectory the output directory - */ - public RestDocumentation(String outputDirectory) { - this.delegate = new JUnitRestDocumentation(outputDirectory); - } - - @Override - public Statement apply(final Statement base, final Description description) { - return this.delegate.apply(base, description); - } - - @Override - public RestDocumentationContext beforeOperation() { - return this.delegate.beforeOperation(); - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java index 4c4de891f..74a1e0386 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.restdocs; import java.io.File; -import java.util.concurrent.atomic.AtomicInteger; /** * {@code RestDocumentationContext} encapsulates the context in which the documentation of @@ -25,77 +24,30 @@ * * @author Andy Wilkinson */ -public final class RestDocumentationContext { - - private final AtomicInteger stepCount = new AtomicInteger(0); - - private final Class testClass; - - private final String testMethodName; - - private final File outputDirectory; - - /** - * Creates a new {@code RestDocumentationContext} for a test on the given - * {@code testClass} with given {@code testMethodName} that will generate - * documentation to the given {@code outputDirectory}. - * - * @param testClass the class whose test is being executed - * @param testMethodName the name of the test method that is being executed - * @param outputDirectory the directory to which documentation should be written. - * @deprecated Since 1.1 in favor of {@link ManualRestDocumentation}. - */ - @Deprecated - public RestDocumentationContext(Class testClass, String testMethodName, - File outputDirectory) { - this.testClass = testClass; - this.testMethodName = testMethodName; - this.outputDirectory = outputDirectory; - } +public interface RestDocumentationContext { /** * Returns the class whose tests are currently executing. - * - * @return The test class + * @return the test class */ - public Class getTestClass() { - return this.testClass; - } + Class getTestClass(); /** * Returns the name of the test method that is currently executing. - * - * @return The name of the test method - */ - public String getTestMethodName() { - return this.testMethodName; - } - - /** - * Returns the current step count and then increments it. - * - * @return The step count prior to it being incremented + * @return the name of the test method */ - int getAndIncrementStepCount() { - return this.stepCount.getAndIncrement(); - } + String getTestMethodName(); /** * Returns the current step count. - * - * @return The current step count + * @return the current step count */ - public int getStepCount() { - return this.stepCount.get(); - } + int getStepCount(); /** * Returns the output directory to which generated snippets should be written. - * * @return the output directory */ - public File getOutputDirectory() { - return this.outputDirectory; - } + File getOutputDirectory(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java index 1db3b92ce..0a83935e1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java @@ -28,7 +28,6 @@ public interface RestDocumentationContextProvider { /** * Returns a {@link RestDocumentationContext} for the operation that is about to be * performed. - * * @return the context for the operation */ RestDocumentationContext beforeOperation(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationExtension.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationExtension.java new file mode 100644 index 000000000..4b7f108dd --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationExtension.java @@ -0,0 +1,98 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package org.springframework.restdocs; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * A JUnit Jupiter {@link Extension} used to automatically manage the + * {@link RestDocumentationContext}. + * + * @author Andy Wilkinson + */ +public class RestDocumentationExtension implements BeforeEachCallback, AfterEachCallback, ParameterResolver { + + private final String outputDirectory; + + /** + * Creates a new {@code RestDocumentationExtension} that will use the default output + * directory. + */ + public RestDocumentationExtension() { + this(null); + } + + /** + * Creates a new {@code RestDocumentationExtension} that will use the given + * {@code outputDirectory}. + * @param outputDirectory snippet output directory + * @since 2.0.4 + */ + public RestDocumentationExtension(String outputDirectory) { + this.outputDirectory = outputDirectory; + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + this.getDelegate(context).beforeTest(context.getRequiredTestClass(), context.getRequiredTestMethod().getName()); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + this.getDelegate(context).afterTest(); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + if (isTestMethodContext(extensionContext)) { + return RestDocumentationContextProvider.class.isAssignableFrom(parameterContext.getParameter().getType()); + } + return false; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext context) { + return (RestDocumentationContextProvider) () -> getDelegate(context).beforeOperation(); + } + + private boolean isTestMethodContext(ExtensionContext context) { + return context.getTestClass().isPresent() && context.getTestMethod().isPresent(); + } + + private ManualRestDocumentation getDelegate(ExtensionContext context) { + Namespace namespace = Namespace.create(getClass(), context.getUniqueId()); + return context.getStore(namespace) + .getOrComputeIfAbsent(ManualRestDocumentation.class, this::createManualRestDocumentation, + ManualRestDocumentation.class); + } + + private ManualRestDocumentation createManualRestDocumentation(Class key) { + if (this.outputDirectory != null) { + return new ManualRestDocumentation(this.outputDirectory); + } + else { + return new ManualRestDocumentation(); + } + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/StandardRestDocumentationContext.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/StandardRestDocumentationContext.java new file mode 100644 index 000000000..2989e1fdb --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/StandardRestDocumentationContext.java @@ -0,0 +1,67 @@ +/* + * Copyright 2014-2019 the original author or 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. + */ + +package org.springframework.restdocs; + +import java.io.File; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Standard implementation of {@link RestDocumentationContext}. + * + * @author Andy Wilkinson + */ +final class StandardRestDocumentationContext implements RestDocumentationContext { + + private final AtomicInteger stepCount = new AtomicInteger(0); + + private final Class testClass; + + private final String testMethodName; + + private final File outputDirectory; + + StandardRestDocumentationContext(Class testClass, String testMethodName, File outputDirectory) { + this.testClass = testClass; + this.testMethodName = testMethodName; + this.outputDirectory = outputDirectory; + } + + @Override + public Class getTestClass() { + return this.testClass; + } + + @Override + public String getTestMethodName() { + return this.testMethodName; + } + + int getAndIncrementStepCount() { + return this.stepCount.getAndIncrement(); + } + + @Override + public int getStepCount() { + return this.stepCount.get(); + } + + @Override + public File getOutputDirectory() { + return this.outputDirectory; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java index 085d9ec86..94531be12 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,10 +27,13 @@ * @author Andy Wilkinson * @author Paul-Christian Volkmer * @author Raman Gupta + * @author Tomasz Kopczynski * @since 1.1.0 */ public abstract class CliDocumentation { + static final CommandFormatter DEFAULT_COMMAND_FORMATTER = multiLineFormat(); + private CliDocumentation() { } @@ -38,45 +41,109 @@ private CliDocumentation() { /** * Returns a new {@code Snippet} that will document the curl request for the API * operation. - * * @return the snippet that will document the curl request */ public static Snippet curlRequest() { - return new CurlRequestSnippet(); + return curlRequest(DEFAULT_COMMAND_FORMATTER); } /** * Returns a new {@code Snippet} that will document the curl request for the API * operation. The given {@code attributes} will be available during snippet * generation. - * * @param attributes the attributes * @return the snippet that will document the curl request */ public static Snippet curlRequest(Map attributes) { - return new CurlRequestSnippet(attributes); + return curlRequest(attributes, DEFAULT_COMMAND_FORMATTER); + } + + /** + * Returns a new {@code Snippet} that will document the curl request for the API + * operation. The given {@code commandFormatter} will be used to format the curl + * command in the snippet. + * @param commandFormatter the command formatter + * @return the snippet that will document the curl request + * @since 1.2.0 + */ + public static Snippet curlRequest(CommandFormatter commandFormatter) { + return curlRequest(null, commandFormatter); + } + + /** + * Returns a new {@code Snippet} that will document the curl request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. The given {@code commandFormatter} will be used to format the curl + * command in the snippet. + * @param attributes the attributes + * @param commandFormatter the command formatter + * @return the snippet that will document the curl request + * @since 1.2.0 + */ + public static Snippet curlRequest(Map attributes, CommandFormatter commandFormatter) { + return new CurlRequestSnippet(attributes, commandFormatter); } /** * Returns a new {@code Snippet} that will document the HTTPie request for the API * operation. - * * @return the snippet that will document the HTTPie request */ public static Snippet httpieRequest() { - return new HttpieRequestSnippet(); + return httpieRequest(DEFAULT_COMMAND_FORMATTER); } /** * Returns a new {@code Snippet} that will document the HTTPie request for the API * operation. The given {@code attributes} will be available during snippet * generation. - * * @param attributes the attributes * @return the snippet that will document the HTTPie request */ public static Snippet httpieRequest(Map attributes) { - return new HttpieRequestSnippet(attributes); + return httpieRequest(attributes, DEFAULT_COMMAND_FORMATTER); + } + + /** + * Returns a new {@code Snippet} that will document the HTTPie request for the API + * operation. The given {@code commandFormatter} will be used to format the HTTPie + * command in the snippet. + * @param commandFormatter the command formatter + * @return the snippet that will document the HTTPie request + * @since 1.2.0 + */ + public static Snippet httpieRequest(CommandFormatter commandFormatter) { + return httpieRequest(null, commandFormatter); + } + + /** + * Returns a new {@code Snippet} that will document the HTTPie request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. The given {@code commandFormatter} will be used to format the HTTPie + * command in the snippet snippet. + * @param attributes the attributes + * @param commandFormatter the command formatter + * @return the snippet that will document the HTTPie request + * @since 1.2.0 + */ + public static Snippet httpieRequest(Map attributes, CommandFormatter commandFormatter) { + return new HttpieRequestSnippet(attributes, commandFormatter); + } + + /** + * Creates a new {@code CommandFormatter} that produces multi-line output. + * @return a multi-line {@code CommandFormatter} + */ + public static CommandFormatter multiLineFormat() { + return new ConcatenatingCommandFormatter(" \\%n "); + } + + /** + * Creates a new {@code CommandFormatter} that produces single-line output. + * @return a single-line {@code CommandFormatter} + */ + public static CommandFormatter singleLineFormat() { + return new ConcatenatingCommandFormatter(" "); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index d04fe18d9..efc21ba4e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.net.URI; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -29,8 +30,7 @@ import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; -import org.springframework.util.Base64Utils; +import org.springframework.restdocs.operation.RequestCookie; /** * An {@link OperationRequest} wrapper with methods that are useful when producing a @@ -47,19 +47,16 @@ final class CliOperationRequest implements OperationRequest { CliOperationRequest(OperationRequest delegate) { this.delegate = delegate; - this.headerFilters = new HashSet<>(Arrays.asList( - new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH), + this.headerFilters = new HashSet<>(Arrays.asList(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH), new BasicAuthHeaderFilter(), new HostHeaderFilter(delegate.getUri()))); } boolean isPutOrPost() { - return HttpMethod.PUT.equals(this.delegate.getMethod()) - || HttpMethod.POST.equals(this.delegate.getMethod()); + return HttpMethod.PUT.equals(this.delegate.getMethod()) || HttpMethod.POST.equals(this.delegate.getMethod()); } String getBasicAuthCredentials() { - List headerValue = this.delegate.getHeaders() - .get(HttpHeaders.AUTHORIZATION); + List headerValue = this.delegate.getHeaders().get(HttpHeaders.AUTHORIZATION); if (BasicAuthHeaderFilter.isBasicAuthHeader(headerValue)) { return BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); } @@ -79,7 +76,7 @@ public String getContentAsString() { @Override public HttpHeaders getHeaders() { HttpHeaders filteredHeaders = new HttpHeaders(); - for (Entry> header : this.delegate.getHeaders().entrySet()) { + for (Entry> header : this.delegate.getHeaders().headerSet()) { if (allowedHeader(header)) { filteredHeaders.put(header.getKey(), header.getValue()); } @@ -93,16 +90,13 @@ private boolean allowedHeader(Map.Entry> header) { return false; } } - if (HttpHeaders.HOST.equalsIgnoreCase(header.getKey()) - && (!header.getValue().isEmpty())) { + if (HttpHeaders.HOST.equalsIgnoreCase(header.getKey()) && (!header.getValue().isEmpty())) { String value = header.getValue().get(0); - if (value.equals(this.delegate.getUri().getHost() + ":" - + this.delegate.getUri().getPort())) { + if (value.equals(this.delegate.getUri().getHost() + ":" + this.delegate.getUri().getPort())) { return false; } } return true; - } @Override @@ -110,11 +104,6 @@ public HttpMethod getMethod() { return this.delegate.getMethod(); } - @Override - public Parameters getParameters() { - return this.delegate.getParameters(); - } - @Override public Collection getParts() { return this.delegate.getParts(); @@ -125,9 +114,15 @@ public URI getUri() { return this.delegate.getUri(); } + @Override + public Collection getCookies() { + return this.delegate.getCookies(); + } + private interface HeaderFilter { boolean allow(String name, List value); + } private static final class BasicAuthHeaderFilter implements HeaderFilter { @@ -138,12 +133,11 @@ public boolean allow(String name, List value) { } static boolean isBasicAuthHeader(List value) { - return value != null && (!value.isEmpty()) - && value.get(0).startsWith("Basic "); + return value != null && (!value.isEmpty()) && value.get(0).startsWith("Basic "); } static String decodeBasicAuthHeader(List value) { - return new String(Base64Utils.decodeFromString(value.get(0).substring(6))); + return new String(Base64.getDecoder().decode(value.get(0).substring(6))); } } @@ -173,13 +167,11 @@ private HostHeaderFilter(URI uri) { @Override public boolean allow(String name, List value) { - return !(value.isEmpty() - || this.getImplicitHostHeader().equals(value.get(0))); + return !(value.isEmpty() || this.getImplicitHostHeader().equals(value.get(0))); } private String getImplicitHostHeader() { - return this.uri.getHost() - + ((this.uri.getPort() == -1) ? "" : ":" + this.uri.getPort()); + return this.uri.getHost() + ((this.uri.getPort() == -1) ? "" : ":" + this.uri.getPort()); } } diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepository.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CommandFormatter.java similarity index 52% rename from samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepository.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CommandFormatter.java index ac781323c..b3d6c1e68 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NoteRepository.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CommandFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,24 @@ * limitations under the License. */ -package com.example.notes; +package org.springframework.restdocs.cli; -import java.util.Collection; import java.util.List; -import org.springframework.data.repository.CrudRepository; - -public interface NoteRepository extends CrudRepository { +/** + * Formatter for CLI commands such as those included in {@link CurlRequestSnippet} and + * {@link HttpieRequestSnippet}. + * + * @author Tomasz Kopczynski + * @since 1.2.0 + */ +public interface CommandFormatter { - Note findById(long id); + /** + * Formats a list of {@code elements} into a single {@code String}. + * @param elements the {@code String} elements to be formatted + * @return a single formatted {@code String} + */ + String format(List elements); - List findByTagsIn(Collection tags); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/ConcatenatingCommandFormatter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/ConcatenatingCommandFormatter.java new file mode 100644 index 000000000..412b8c00c --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/ConcatenatingCommandFormatter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2018 the original author or 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. + */ + +package org.springframework.restdocs.cli; + +import java.util.List; + +import org.springframework.util.CollectionUtils; + +/** + * {@link CommandFormatter} which concatenates commands with a given {@code separator}. + * + * @author Tomasz Kopczynski + */ +final class ConcatenatingCommandFormatter implements CommandFormatter { + + private String separator; + + ConcatenatingCommandFormatter(String separator) { + this.separator = separator; + } + + /** + * Concatenates a list of {@code String}s with a specified separator. + * @param elements a list of {@code String}s to be concatenated + * @return concatenated list of {@code String}s as one {@code String} + */ + @Override + public String format(List elements) { + if (CollectionUtils.isEmpty(elements)) { + return ""; + } + StringBuilder result = new StringBuilder(); + for (String element : elements) { + result.append(String.format(this.separator)); + result.append(element); + } + return result.toString(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 72fc9e7ea..469973aee 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,22 @@ package org.springframework.restdocs.cli; -import java.io.PrintWriter; -import java.io.StringWriter; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import org.springframework.http.HttpMethod; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -37,27 +39,36 @@ * * @author Andy Wilkinson * @author Paul-Christian Volkmer + * @author Tomasz Kopczynski * @since 1.1.0 * @see CliDocumentation#curlRequest() + * @see CliDocumentation#curlRequest(CommandFormatter) * @see CliDocumentation#curlRequest(Map) */ public class CurlRequestSnippet extends TemplatedSnippet { + private final CommandFormatter commandFormatter; + /** - * Creates a new {@code CurlRequestSnippet} with no additional attributes. + * Creates a new {@code CurlRequestSnippet} that will use the given + * {@code commandFormatter} to format the curl command. + * @param commandFormatter the formatter */ - protected CurlRequestSnippet() { - this(null); + protected CurlRequestSnippet(CommandFormatter commandFormatter) { + this(null, commandFormatter); } /** * Creates a new {@code CurlRequestSnippet} with the given additional * {@code attributes} that will be included in the model during template rendering. - * - * @param attributes The additional attributes + * The given {@code commandFormaatter} will be used to format the curl command. + * @param attributes the additional attributes + * @param commandFormatter the formatter for generating the snippet */ - protected CurlRequestSnippet(Map attributes) { + protected CurlRequestSnippet(Map attributes, CommandFormatter commandFormatter) { super("curl-request", attributes); + Assert.notNull(commandFormatter, "Command formatter must not be null"); + this.commandFormatter = commandFormatter; } @Override @@ -70,103 +81,91 @@ protected Map createModel(Operation operation) { private String getUrl(Operation operation) { OperationRequest request = operation.getRequest(); - Parameters uniqueParameters = request.getParameters() - .getUniqueParameters(operation.getRequest().getUri()); - if (!uniqueParameters.isEmpty() && includeParametersInUri(request)) { - return String.format("'%s%s%s'", request.getUri(), - StringUtils.hasText(request.getUri().getRawQuery()) ? "&" : "?", - uniqueParameters.toQueryString()); - } return String.format("'%s'", request.getUri()); } - private boolean includeParametersInUri(OperationRequest request) { - return request.getMethod() == HttpMethod.GET || request.getContent().length > 0; - } - private String getOptions(Operation operation) { - StringWriter command = new StringWriter(); - PrintWriter printer = new PrintWriter(command); - writeIncludeHeadersInOutputOption(printer); + StringBuilder builder = new StringBuilder(); + writeIncludeHeadersInOutputOption(builder); + CliOperationRequest request = new CliOperationRequest(operation.getRequest()); - writeUserOptionIfNecessary(request, printer); - writeHttpMethodIfNecessary(request, printer); - writeHeaders(request, printer); - writePartsIfNecessary(request, printer); - writeContent(request, printer); + writeUserOptionIfNecessary(request, builder); + writeHttpMethod(request, builder); + + List additionalLines = new ArrayList<>(); + writeHeaders(request, additionalLines); + writeCookies(request, additionalLines); + writePartsIfNecessary(request, additionalLines); + writeContent(request, additionalLines); - return command.toString(); + builder.append(this.commandFormatter.format(additionalLines)); + + return builder.toString(); + } + + private void writeCookies(CliOperationRequest request, List lines) { + if (!CollectionUtils.isEmpty(request.getCookies())) { + StringBuilder cookiesBuilder = new StringBuilder(); + for (RequestCookie cookie : request.getCookies()) { + if (cookiesBuilder.length() > 0) { + cookiesBuilder.append(";"); + } + cookiesBuilder.append(String.format("%s=%s", cookie.getName(), cookie.getValue())); + } + lines.add(String.format("--cookie '%s'", cookiesBuilder.toString())); + } } - private void writeIncludeHeadersInOutputOption(PrintWriter writer) { - writer.print("-i"); + private void writeIncludeHeadersInOutputOption(StringBuilder builder) { + builder.append("-i"); } - private void writeUserOptionIfNecessary(CliOperationRequest request, - PrintWriter writer) { + private void writeUserOptionIfNecessary(CliOperationRequest request, StringBuilder builder) { String credentials = request.getBasicAuthCredentials(); if (credentials != null) { - writer.print(String.format(" -u '%s'", credentials)); + builder.append(String.format(" -u '%s'", credentials)); } } - private void writeHttpMethodIfNecessary(OperationRequest request, - PrintWriter writer) { - if (!HttpMethod.GET.equals(request.getMethod())) { - writer.print(String.format(" -X %s", request.getMethod())); - } + private void writeHttpMethod(OperationRequest request, StringBuilder builder) { + builder.append(String.format(" -X %s", request.getMethod())); } - private void writeHeaders(CliOperationRequest request, PrintWriter writer) { - for (Entry> entry : request.getHeaders().entrySet()) { + private void writeHeaders(CliOperationRequest request, List lines) { + for (Entry> entry : request.getHeaders().headerSet()) { for (String header : entry.getValue()) { - writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); + if (StringUtils.hasText(request.getContentAsString()) && HttpHeaders.CONTENT_TYPE.equals(entry.getKey()) + && MediaType.APPLICATION_FORM_URLENCODED.equals(request.getHeaders().getContentType())) { + continue; + } + lines.add(String.format("-H '%s: %s'", entry.getKey(), header)); } } } - private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) { + private void writePartsIfNecessary(OperationRequest request, List lines) { for (OperationRequestPart part : request.getParts()) { - writer.printf(" -F '%s=", part.getName()); + StringBuilder oneLine = new StringBuilder(); + oneLine.append(String.format("-F '%s=", part.getName())); if (!StringUtils.hasText(part.getSubmittedFileName())) { - writer.append(part.getContentAsString()); + oneLine.append(part.getContentAsString()); } else { - writer.printf("@%s", part.getSubmittedFileName()); + oneLine.append(String.format("@%s", part.getSubmittedFileName())); } if (part.getHeaders().getContentType() != null) { - writer.append(";type=") - .append(part.getHeaders().getContentType().toString()); + oneLine.append(";type="); + oneLine.append(part.getHeaders().getContentType().toString()); } - - writer.append("'"); + oneLine.append("'"); + lines.add(oneLine.toString()); } } - private void writeContent(CliOperationRequest request, PrintWriter writer) { + private void writeContent(CliOperationRequest request, List lines) { String content = request.getContentAsString(); if (StringUtils.hasText(content)) { - writer.print(String.format(" -d '%s'", content)); - } - else if (!request.getParts().isEmpty()) { - for (Entry> entry : request.getParameters().entrySet()) { - for (String value : entry.getValue()) { - writer.print(String.format(" -F '%s=%s'", entry.getKey(), value)); - } - } - } - else if (request.isPutOrPost()) { - writeContentUsingParameters(request, writer); - } - } - - private void writeContentUsingParameters(OperationRequest request, - PrintWriter writer) { - Parameters uniqueParameters = request.getParameters() - .getUniqueParameters(request.getUri()); - String queryString = uniqueParameters.toQueryString(); - if (StringUtils.hasText(queryString)) { - writer.print(String.format(" -d '%s'", queryString)); + lines.add(String.format("-d '%s'", content)); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index c262015b5..8b92fa0bf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,22 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.FormParameters; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -39,27 +41,35 @@ * * @author Raman Gupta * @author Andy Wilkinson + * @author Tomasz Kopczynski * @since 1.1.0 * @see CliDocumentation#httpieRequest() * @see CliDocumentation#httpieRequest(Map) */ public class HttpieRequestSnippet extends TemplatedSnippet { + private final CommandFormatter commandFormatter; + /** - * Creates a new {@code HttpieRequestSnippet} with no additional attributes. + * Creates a new {@code HttpieRequestSnippet} that will use the given + * {@code commandFormatter} to format the HTTPie command. + * @param commandFormatter the formatter */ - protected HttpieRequestSnippet() { - this(null); + protected HttpieRequestSnippet(CommandFormatter commandFormatter) { + this(null, commandFormatter); } /** * Creates a new {@code HttpieRequestSnippet} with the given additional * {@code attributes} that will be included in the model during template rendering. - * - * @param attributes The additional attributes + * The given {@code commandFormaatter} will be used to format the HTTPie command. + * @param attributes the additional attributes + * @param commandFormatter the formatter for generating the snippet */ - protected HttpieRequestSnippet(Map attributes) { + protected HttpieRequestSnippet(Map attributes, CommandFormatter commandFormatter) { super("httpie-request", attributes); + Assert.notNull(commandFormatter, "Command formatter must not be null"); + this.commandFormatter = commandFormatter; } @Override @@ -74,6 +84,9 @@ protected Map createModel(Operation operation) { } private Object getContentStandardIn(CliOperationRequest request) { + if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(request.getHeaders().getContentType())) { + return ""; + } String content = request.getContentAsString(); if (StringUtils.hasText(content)) { return String.format("echo '%s' | ", content); @@ -91,39 +104,29 @@ private String getOptions(CliOperationRequest request) { } private String getUrl(OperationRequest request) { - Parameters uniqueParameters = request.getParameters() - .getUniqueParameters(request.getUri()); - if (!uniqueParameters.isEmpty() && includeParametersInUri(request)) { - return String.format("'%s%s%s'", request.getUri(), - StringUtils.hasText(request.getUri().getRawQuery()) ? "&" : "?", - uniqueParameters.toQueryString()); - } return String.format("'%s'", request.getUri()); } private String getRequestItems(CliOperationRequest request) { - StringWriter requestItems = new StringWriter(); - PrintWriter printer = new PrintWriter(requestItems); - writeFormDataIfNecessary(request, printer); - writeHeaders(request, printer); - writeParametersIfNecessary(request, printer); - return requestItems.toString(); + List lines = new ArrayList<>(); + + writeHeaders(request, lines); + writeCookies(request, lines); + writeFormDataIfNecessary(request, lines); + + return this.commandFormatter.format(lines); } private void writeOptions(OperationRequest request, PrintWriter writer) { - if (!request.getParts().isEmpty() - || (!request.getParameters().getUniqueParameters(request.getUri()) - .isEmpty() && !includeParametersInUri(request))) { + if (!request.getParts().isEmpty()) { + writer.print("--multipart "); + } + else if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(request.getHeaders().getContentType())) { writer.print("--form "); } } - private boolean includeParametersInUri(OperationRequest request) { - return request.getMethod() == HttpMethod.GET || request.getContent().length > 0; - } - - private void writeUserOptionIfNecessary(CliOperationRequest request, - PrintWriter writer) { + private void writeUserOptionIfNecessary(CliOperationRequest request, PrintWriter writer) { String credentials = request.getBasicAuthCredentials(); if (credentials != null) { writer.print(String.format("--auth '%s' ", credentials)); @@ -134,59 +137,49 @@ private void writeMethodIfNecessary(OperationRequest request, PrintWriter writer writer.print(String.format("%s", request.getMethod().name())); } - private void writeFormDataIfNecessary(OperationRequest request, PrintWriter writer) { - for (OperationRequestPart part : request.getParts()) { - writer.printf(" \\%n '%s'", part.getName()); - if (!StringUtils.hasText(part.getSubmittedFileName())) { - // https://github.com/jkbrzt/httpie/issues/342 - writer.printf("@<(echo '%s')", part.getContentAsString()); - } - else { - writer.printf("@'%s'", part.getSubmittedFileName()); + private void writeFormDataIfNecessary(OperationRequest request, List lines) { + if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(request.getHeaders().getContentType())) { + FormParameters.from(request) + .forEach((key, values) -> values.forEach((value) -> lines.add(String.format("'%s=%s'", key, value)))); + } + else { + for (OperationRequestPart part : request.getParts()) { + StringBuilder oneLine = new StringBuilder(); + oneLine.append(String.format("'%s'", part.getName())); + if (!StringUtils.hasText(part.getSubmittedFileName())) { + oneLine.append(String.format("='%s'", part.getContentAsString())); + } + else { + oneLine.append(String.format("@'%s'", part.getSubmittedFileName())); + } + + lines.add(oneLine.toString()); } } } - private void writeHeaders(OperationRequest request, PrintWriter writer) { + private void writeHeaders(OperationRequest request, List lines) { HttpHeaders headers = request.getHeaders(); - for (Entry> entry : headers.entrySet()) { + for (Entry> entry : headers.headerSet()) { + if (entry.getKey().equals(HttpHeaders.CONTENT_TYPE) + && headers.getContentType().isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)) { + continue; + } for (String header : entry.getValue()) { // HTTPie adds Content-Type automatically with --form - if (!request.getParts().isEmpty() - && entry.getKey().equals(HttpHeaders.CONTENT_TYPE) + if (!request.getParts().isEmpty() && entry.getKey().equals(HttpHeaders.CONTENT_TYPE) && header.startsWith(MediaType.MULTIPART_FORM_DATA_VALUE)) { continue; } - writer.print(String.format(" '%s:%s'", entry.getKey(), header)); + lines.add(String.format("'%s:%s'", entry.getKey(), header)); } } } - private void writeParametersIfNecessary(CliOperationRequest request, - PrintWriter writer) { - if (StringUtils.hasText(request.getContentAsString())) { - return; - } - if (!request.getParts().isEmpty()) { - writeContentUsingParameters(request.getParameters(), writer); - } - else if (request.isPutOrPost()) { - writeContentUsingParameters( - request.getParameters().getUniqueParameters(request.getUri()), - writer); + private void writeCookies(OperationRequest request, List lines) { + for (RequestCookie cookie : request.getCookies()) { + lines.add(String.format("'Cookie:%s=%s'", cookie.getName(), cookie.getValue())); } } - private void writeContentUsingParameters(Parameters parameters, PrintWriter writer) { - for (Map.Entry> entry : parameters.entrySet()) { - if (entry.getValue().isEmpty()) { - writer.append(String.format(" '%s='", entry.getKey())); - } - else { - for (String value : entry.getValue()) { - writer.append(String.format(" '%s=%s'", entry.getKey(), value)); - } - } - } - } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java index 118a4ef23..39bfd6d3f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,11 +31,9 @@ public abstract class AbstractConfigurer { /** * Applies the configurer to the given {@code configuration}. - * * @param configuration the configuration to be configured * @param context the current documentation context */ - public abstract void apply(Map configuration, - RestDocumentationContext context); + public abstract void apply(Map configuration, RestDocumentationContext context); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java index fff757160..c5327c864 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,11 @@ /** * Base class for {@link NestedConfigurer} implementations. * - * @param The type of the configurer's parent + * @param the type of the configurer's parent * @author Andy Wilkinson * @since 1.1.0 */ -public abstract class AbstractNestedConfigurer extends AbstractConfigurer - implements NestedConfigurer { +public abstract class AbstractNestedConfigurer extends AbstractConfigurer implements NestedConfigurer { private final PARENT parent; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java index d5b133fc8..8f26d2f95 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ /** * A configurer that is nested and, therefore, has a parent. * - * @param The parent's type + * @param the parent's type * @author Andy Wilkinson * @since 1.1.0 */ @@ -27,8 +27,8 @@ interface NestedConfigurer { /** * Returns the configurer's parent. - * * @return the parent */ PARENT and(); + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/OperationPreprocessorsConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/OperationPreprocessorsConfigurer.java new file mode 100644 index 000000000..56b5b5ed9 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/OperationPreprocessorsConfigurer.java @@ -0,0 +1,82 @@ +/* + * Copyright 2014-2019 the original author or 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. + */ + +package org.springframework.restdocs.config; + +import java.util.Map; + +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.generate.RestDocumentationGenerator; +import org.springframework.restdocs.operation.preprocess.OperationPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.operation.preprocess.Preprocessors; + +/** + * A configurer that can be used to configure the default operation preprocessors. + * + * @param the type of the configurer's parent + * @param the concrete type of the configurer to be returned from chained methods + * @author Filip Hrisafov + * @author Andy Wilkinson + * @since 2.0.0 + */ +public abstract class OperationPreprocessorsConfigurer extends AbstractNestedConfigurer { + + private OperationRequestPreprocessor defaultOperationRequestPreprocessor; + + private OperationResponsePreprocessor defaultOperationResponsePreprocessor; + + /** + * Creates a new {@code OperationPreprocessorConfigurer} with the given + * {@code parent}. + * @param parent the parent + */ + protected OperationPreprocessorsConfigurer(PARENT parent) { + super(parent); + } + + @Override + public void apply(Map configuration, RestDocumentationContext context) { + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_REQUEST_PREPROCESSOR, + this.defaultOperationRequestPreprocessor); + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR, + this.defaultOperationResponsePreprocessor); + } + + /** + * Configures the default operation request preprocessors. + * @param preprocessors the preprocessors + * @return {@code this} + */ + @SuppressWarnings("unchecked") + public TYPE withRequestDefaults(OperationPreprocessor... preprocessors) { + this.defaultOperationRequestPreprocessor = Preprocessors.preprocessRequest(preprocessors); + return (TYPE) this; + } + + /** + * Configures the default operation response preprocessors. + * @param preprocessors the preprocessors + * @return {@code this} + */ + @SuppressWarnings("unchecked") + public TYPE withResponseDefaults(OperationPreprocessor... preprocessors) { + this.defaultOperationResponsePreprocessor = Preprocessors.preprocessResponse(preprocessors); + return (TYPE) this; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index d56428cca..3c49684f7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.restdocs.config; +import java.nio.charset.Charset; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -35,13 +36,15 @@ /** * Abstract base class for the configuration of Spring REST Docs. * - * @param The concrete type of the {@link SnippetConfigurer}. - * @param The concrete type of this configurer, to be returned from methods that + * @param the concrete type of the {@link SnippetConfigurer} + * @param

          the concrete type of the {@link OperationPreprocessorsConfigurer} + * @param the concrete type of this configurer, to be returned from methods that * support chaining * @author Andy Wilkinson + * @author Filip Hrisafov * @since 1.1.0 */ -public abstract class RestDocumentationConfigurer { +public abstract class RestDocumentationConfigurer { private final WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer(); @@ -50,14 +53,19 @@ public abstract class RestDocumentationConfigurer configuration, - RestDocumentationContext context) { - List configurers = Arrays.asList(snippets(), + protected final void apply(Map configuration, RestDocumentationContext context) { + List configurers = Arrays.asList(snippets(), operationPreprocessors(), this.templateEngineConfigurer, this.writerResolverConfigurer); for (AbstractConfigurer configurer : configurers) { configurer.apply(configuration, context); @@ -101,22 +106,19 @@ private static final class TemplateEngineConfigurer extends AbstractConfigurer { private TemplateEngine templateEngine; @Override - public void apply(Map configuration, - RestDocumentationContext context) { + public void apply(Map configuration, RestDocumentationContext context) { TemplateEngine engineToUse = this.templateEngine; if (engineToUse == null) { SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration - .get(SnippetConfiguration.class.getName()); + .get(SnippetConfiguration.class.getName()); Map templateContext = new HashMap<>(); - if (snippetConfiguration.getTemplateFormat().getId() - .equals(TemplateFormats.asciidoctor().getId())) { - templateContext.put("tableCellContent", - new AsciidoctorTableCellContentLambda()); + if (snippetConfiguration.getTemplateFormat().getId().equals(TemplateFormats.asciidoctor().getId())) { + templateContext.put("tableCellContent", new AsciidoctorTableCellContentLambda()); } engineToUse = new MustacheTemplateEngine( - new StandardTemplateResourceResolver( - snippetConfiguration.getTemplateFormat()), - Mustache.compiler().escapeHTML(false), templateContext); + new StandardTemplateResourceResolver(snippetConfiguration.getTemplateFormat()), + Charset.forName(snippetConfiguration.getEncoding()), Mustache.compiler().escapeHTML(false), + templateContext); } configuration.put(TemplateEngine.class.getName(), engineToUse); } @@ -132,16 +134,13 @@ private static final class WriterResolverConfigurer extends AbstractConfigurer { private WriterResolver writerResolver; @Override - public void apply(Map configuration, - RestDocumentationContext context) { + public void apply(Map configuration, RestDocumentationContext context) { WriterResolver resolverToUse = this.writerResolver; if (resolverToUse == null) { SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration - .get(SnippetConfiguration.class.getName()); - resolverToUse = new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolverFactory(), - snippetConfiguration.getEncoding(), - snippetConfiguration.getTemplateFormat()); + .get(SnippetConfiguration.class.getName()); + resolverToUse = new StandardWriterResolver(new RestDocumentationContextPlaceholderResolverFactory(), + snippetConfiguration.getEncoding(), snippetConfiguration.getTemplateFormat()); } configuration.put(WriterResolver.class.getName(), resolverToUse); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index 6e3c27658..a21b63146 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.restdocs.cli.CliDocumentation; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpDocumentation; +import org.springframework.restdocs.payload.PayloadDocumentation; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; @@ -32,17 +33,16 @@ /** * A configurer that can be used to configure the generated documentation snippets. * - * @param The type of the configurer's parent - * @param The concrete type of the configurer to be returned from chained methods + * @param the type of the configurer's parent + * @param the concrete type of the configurer to be returned from chained methods * @author Andy Wilkinson * @since 1.1.0 */ -public abstract class SnippetConfigurer - extends AbstractNestedConfigurer { +public abstract class SnippetConfigurer extends AbstractNestedConfigurer { - private List defaultSnippets = new ArrayList<>(Arrays.asList( - CliDocumentation.curlRequest(), CliDocumentation.httpieRequest(), - HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse())); + private List defaultSnippets = new ArrayList<>(Arrays.asList(CliDocumentation.curlRequest(), + CliDocumentation.httpieRequest(), HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse(), + PayloadDocumentation.requestBody(), PayloadDocumentation.responseBody())); /** * The default encoding for documentation snippets. @@ -56,8 +56,7 @@ public abstract class SnippetConfigurer * * @see #withTemplateFormat(TemplateFormat) */ - public static final TemplateFormat DEFAULT_TEMPLATE_FORMAT = TemplateFormats - .asciidoctor(); + public static final TemplateFormat DEFAULT_TEMPLATE_FORMAT = TemplateFormats.asciidoctor(); private String snippetEncoding = DEFAULT_SNIPPET_ENCODING; @@ -65,7 +64,6 @@ public abstract class SnippetConfigurer /** * Creates a new {@code SnippetConfigurer} with the given {@code parent}. - * * @param parent the parent */ protected SnippetConfigurer(PARENT parent) { @@ -73,18 +71,15 @@ protected SnippetConfigurer(PARENT parent) { } @Override - public void apply(Map configuration, - RestDocumentationContext context) { + public void apply(Map configuration, RestDocumentationContext context) { configuration.put(SnippetConfiguration.class.getName(), new SnippetConfiguration(this.snippetEncoding, this.templateFormat)); - configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS, - this.defaultSnippets); + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS, this.defaultSnippets); } /** * Configures any documentation snippets to be written using the given * {@code encoding}. The default is UTF-8. - * * @param encoding the encoding * @return {@code this} */ @@ -96,7 +91,6 @@ public TYPE withEncoding(String encoding) { /** * Configures the documentation snippets that will be produced by default. - * * @param defaultSnippets the default snippets * @return {@code this} * @see #withAdditionalDefaults(Snippet...) @@ -109,7 +103,6 @@ public TYPE withDefaults(Snippet... defaultSnippets) { /** * Configures additional documentation snippets that will be produced by default. - * * @param additionalDefaultSnippets the additional default snippets * @return {@code this} * @see #withDefaults(Snippet...) @@ -122,7 +115,6 @@ public TYPE withAdditionalDefaults(Snippet... additionalDefaultSnippets) { /** * Configures the format of the documentation snippet templates. - * * @param format the snippet template format * @return {@code this} */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/Constraint.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/Constraint.java index d8cf77471..3ad467432 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/Constraint.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/Constraint.java @@ -32,7 +32,6 @@ public class Constraint { /** * Creates a new {@code Constraint} with the given {@code name} and * {@code configuration}. - * * @param name the name * @param configuration the configuration */ @@ -43,7 +42,6 @@ public Constraint(String name, Map configuration) { /** * Returns the name of the constraint. - * * @return the name */ public String getName() { @@ -52,7 +50,6 @@ public String getName() { /** * Returns the configuration of the constraint. - * * @return the configuration */ public Map getConfiguration() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java index 7fa30ecef..6291f9a87 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,15 +20,14 @@ * Resolves a description for a {@link Constraint}. * * @author Andy Wilkinson - * */ public interface ConstraintDescriptionResolver { /** * Resolves the description for the given {@code constraint}. - * * @param constraint the constraint * @return the description */ String resolveDescription(Constraint constraint); + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java index 548dd1634..7df2bb0c9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,12 +38,10 @@ public class ConstraintDescriptions { * Constraints will be resolved using a {@link ValidatorConstraintResolver} and * descriptions will be resolved using a * {@link ResourceBundleConstraintDescriptionResolver}. - * * @param clazz the class */ public ConstraintDescriptions(Class clazz) { - this(clazz, new ValidatorConstraintResolver(), - new ResourceBundleConstraintDescriptionResolver()); + this(clazz, new ValidatorConstraintResolver(), new ResourceBundleConstraintDescriptionResolver()); } /** @@ -51,25 +49,21 @@ public ConstraintDescriptions(Class clazz) { * Constraints will be resolved using the given {@code constraintResolver} and * descriptions will be resolved using a * {@link ResourceBundleConstraintDescriptionResolver}. - * * @param clazz the class * @param constraintResolver the constraint resolver */ public ConstraintDescriptions(Class clazz, ConstraintResolver constraintResolver) { - this(clazz, constraintResolver, - new ResourceBundleConstraintDescriptionResolver()); + this(clazz, constraintResolver, new ResourceBundleConstraintDescriptionResolver()); } /** * Create a new {@code ConstraintDescriptions} for the given {@code clazz}. * Constraints will be resolved using a {@link ValidatorConstraintResolver} and * descriptions will be resolved using the given {@code descriptionResolver}. - * * @param clazz the class * @param descriptionResolver the description resolver */ - public ConstraintDescriptions(Class clazz, - ConstraintDescriptionResolver descriptionResolver) { + public ConstraintDescriptions(Class clazz, ConstraintDescriptionResolver descriptionResolver) { this(clazz, new ValidatorConstraintResolver(), descriptionResolver); } @@ -77,7 +71,6 @@ public ConstraintDescriptions(Class clazz, * Create a new {@code ConstraintDescriptions} for the given {@code clazz}. * Constraints will be resolved using the given {@code constraintResolver} and * descriptions will be resolved using the given {@code descriptionResolver}. - * * @param clazz the class * @param constraintResolver the constraint resolver * @param descriptionResolver the description resolver @@ -91,13 +84,11 @@ public ConstraintDescriptions(Class clazz, ConstraintResolver constraintResol /** * Returns a list of the descriptions for the constraints on the given property. - * * @param property the property * @return the list of constraint descriptions */ public List descriptionsForProperty(String property) { - List constraints = this.constraintResolver - .resolveForProperty(property, this.clazz); + List constraints = this.constraintResolver.resolveForProperty(property, this.clazz); List descriptions = new ArrayList<>(); for (Constraint constraint : constraints) { descriptions.add(this.descriptionResolver.resolveDescription(constraint)); @@ -105,4 +96,5 @@ public List descriptionsForProperty(String property) { Collections.sort(descriptions); return descriptions; } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java index ee6e7292a..f5e6079d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintResolver.java @@ -28,7 +28,6 @@ public interface ConstraintResolver { /** * Resolves and returns the constraints for the given {@code property} on the given * {@code clazz}. If there are no constraints, an empty list is returned. - * * @param property the property * @param clazz the class * @return the list of constraints, never {@code null} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java index 61b187c26..59788d061 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,44 +20,51 @@ import java.util.MissingResourceException; import java.util.ResourceBundle; -import javax.validation.constraints.AssertFalse; -import javax.validation.constraints.AssertTrue; -import javax.validation.constraints.DecimalMax; -import javax.validation.constraints.DecimalMin; -import javax.validation.constraints.Digits; -import javax.validation.constraints.Future; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Null; -import javax.validation.constraints.Past; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Size; - +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Negative; +import jakarta.validation.constraints.NegativeOrZero; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Null; +import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; +import org.hibernate.validator.constraints.CodePointLength; import org.hibernate.validator.constraints.CreditCardNumber; +import org.hibernate.validator.constraints.Currency; import org.hibernate.validator.constraints.EAN; -import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.LuhnCheck; import org.hibernate.validator.constraints.Mod10Check; import org.hibernate.validator.constraints.Mod11Check; -import org.hibernate.validator.constraints.NotBlank; -import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.Range; -import org.hibernate.validator.constraints.SafeHtml; import org.hibernate.validator.constraints.URL; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; +import org.springframework.util.StringUtils; /** * A {@link ConstraintDescriptionResolver} that resolves constraint descriptions from a * {@link ResourceBundle}. The resource bundle's keys are the name of the constraint with * {@code .description} appended. For example, the key for the constraint named - * {@code javax.validation.constraints.NotNull} is - * {@code javax.validation.constraints.NotNull.description}. + * {@code jakarta.validation.constraints.NotNull} is + * {@code jakarta.validation.constraints.NotNull.description}. *

          - * Default descriptions are provided for Bean Validation 1.1's constraints: + * Default descriptions are provided for all of Bean Validation 3.1's constraints: * *

            *
          • {@link AssertFalse} @@ -65,41 +72,47 @@ *
          • {@link DecimalMax} *
          • {@link DecimalMin} *
          • {@link Digits} + *
          • {@link Email} *
          • {@link Future} + *
          • {@link FutureOrPresent} *
          • {@link Max} *
          • {@link Min} + *
          • {@link Negative} + *
          • {@link NegativeOrZero} + *
          • {@link NotBlank} + *
          • {@link NotEmpty} *
          • {@link NotNull} *
          • {@link Null} *
          • {@link Past} + *
          • {@link PastOrPresent} *
          • {@link Pattern} + *
          • {@link Positive} + *
          • {@link PositiveOrZero} *
          • {@link Size} *
          * *

          - * Default descriptions are also provided for Hibernate Validator's constraints: + * Default descriptions are also provided for the following Hibernate Validator + * constraints: * *

            + *
          • {@link CodePointLength} *
          • {@link CreditCardNumber} + *
          • {@link Currency} *
          • {@link EAN} - *
          • {@link Email} *
          • {@link Length} *
          • {@link LuhnCheck} *
          • {@link Mod10Check} *
          • {@link Mod11Check} - *
          • {@link NotBlank} - *
          • {@link NotEmpty} *
          • {@link Range} - *
          • {@link SafeHtml} *
          • {@link URL} *
          * * @author Andy Wilkinson */ -public class ResourceBundleConstraintDescriptionResolver - implements ConstraintDescriptionResolver { +public class ResourceBundleConstraintDescriptionResolver implements ConstraintDescriptionResolver { - private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper( - "${", "}"); + private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}"); private final ResourceBundle defaultDescriptions; @@ -118,7 +131,6 @@ public ResourceBundleConstraintDescriptionResolver() { /** * Creates a new {@code ResourceBundleConstraintDescriptionResolver} that will resolve * descriptions by looking them up in the given {@code resourceBundle}. - * * @param resourceBundle the resource bundle */ public ResourceBundleConstraintDescriptionResolver(ResourceBundle resourceBundle) { @@ -129,8 +141,7 @@ public ResourceBundleConstraintDescriptionResolver(ResourceBundle resourceBundle private static ResourceBundle getBundle(String name) { try { return ResourceBundle.getBundle( - ResourceBundleConstraintDescriptionResolver.class.getPackage() - .getName() + "." + name, + ResourceBundleConstraintDescriptionResolver.class.getPackage().getName() + "." + name, Locale.getDefault(), Thread.currentThread().getContextClassLoader()); } catch (MissingResourceException ex) { @@ -157,8 +168,7 @@ private String getDescription(String key) { return this.defaultDescriptions.getString(key); } - private static final class ConstraintPlaceholderResolver - implements PlaceholderResolver { + private static final class ConstraintPlaceholderResolver implements PlaceholderResolver { private final Constraint constraint; @@ -169,7 +179,13 @@ private ConstraintPlaceholderResolver(Constraint constraint) { @Override public String resolvePlaceholder(String placeholderName) { Object replacement = this.constraint.getConfiguration().get(placeholderName); - return replacement != null ? replacement.toString() : null; + if (replacement == null) { + return null; + } + if (replacement.getClass().isArray()) { + return StringUtils.arrayToDelimitedString((Object[]) replacement, ", "); + } + return replacement.toString(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java index 9dc82b9b0..28910a938 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,19 +19,19 @@ import java.util.ArrayList; import java.util.List; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import javax.validation.constraints.NotNull; -import javax.validation.metadata.BeanDescriptor; -import javax.validation.metadata.ConstraintDescriptor; -import javax.validation.metadata.PropertyDescriptor; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.metadata.BeanDescriptor; +import jakarta.validation.metadata.ConstraintDescriptor; +import jakarta.validation.metadata.PropertyDescriptor; /** * A {@link ConstraintResolver} that uses a Bean Validation {@link Validator} to resolve * constraints. The name of the constraint is the fully-qualified class name of the * constraint annotation. For example, a {@link NotNull} constraint will be named - * {@code javax.validation.constraints.NotNull}. + * {@code jakarta.validation.constraints.NotNull}. * * @author Andy Wilkinson * @@ -54,7 +54,6 @@ public ValidatorConstraintResolver() { /** * Creates a new {@code ValidatorConstraintResolver} that will use the given * {@code Validator} to resolve constraints. - * * @param validator the validator */ public ValidatorConstraintResolver(Validator validator) { @@ -65,16 +64,14 @@ public ValidatorConstraintResolver(Validator validator) { public List resolveForProperty(String property, Class clazz) { List constraints = new ArrayList<>(); BeanDescriptor beanDescriptor = this.validator.getConstraintsForClass(clazz); - PropertyDescriptor propertyDescriptor = beanDescriptor - .getConstraintsForProperty(property); + PropertyDescriptor propertyDescriptor = beanDescriptor.getConstraintsForProperty(property); if (propertyDescriptor != null) { - for (ConstraintDescriptor constraintDescriptor : propertyDescriptor - .getConstraintDescriptors()) { - constraints.add(new Constraint( - constraintDescriptor.getAnnotation().annotationType().getName(), + for (ConstraintDescriptor constraintDescriptor : propertyDescriptor.getConstraintDescriptors()) { + constraints.add(new Constraint(constraintDescriptor.getAnnotation().annotationType().getName(), constraintDescriptor.getAttributes())); } } return constraints; } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java new file mode 100644 index 000000000..19201586b --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java @@ -0,0 +1,158 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package org.springframework.restdocs.cookies; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Assert; + +/** + * Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that + * document a RESTful resource's request or response cookies. + * + * @author Clyde Stubbs + * @author Andy Wilkinson + * @since 3.0 + */ +public abstract class AbstractCookiesSnippet extends TemplatedSnippet { + + private final Map descriptorsByName = new LinkedHashMap<>(); + + private final boolean ignoreUndocumentedCookies; + + /** + * Creates a new {@code AbstractCookiesSnippet} that will produce a snippet named + * {@code -cookies}. The cookies will be documented using the given + * {@code descriptors} and the given {@code attributes} will be included in the model + * during template rendering. + * @param type the type of the cookies + * @param descriptors the cookie descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedCookies whether undocumented cookies should be ignored + */ + protected AbstractCookiesSnippet(String type, List descriptors, Map attributes, + boolean ignoreUndocumentedCookies) { + super(type + "-cookies", attributes); + for (CookieDescriptor descriptor : descriptors) { + Assert.notNull(descriptor.getName(), "Cookie descriptors must have a name"); + if (!descriptor.isIgnored()) { + Assert.notNull(descriptor.getDescription(), "The descriptor for cookie '" + descriptor.getName() + + "' must either have a description or be marked as ignored"); + } + this.descriptorsByName.put(descriptor.getName(), descriptor); + } + this.ignoreUndocumentedCookies = ignoreUndocumentedCookies; + } + + @Override + protected Map createModel(Operation operation) { + verifyCookieDescriptors(operation); + + Map model = new HashMap<>(); + List> cookies = new ArrayList<>(); + for (CookieDescriptor descriptor : this.descriptorsByName.values()) { + if (!descriptor.isIgnored()) { + cookies.add(createModelForDescriptor(descriptor)); + } + } + model.put("cookies", cookies); + return model; + } + + private void verifyCookieDescriptors(Operation operation) { + Set actualCookies = extractActualCookies(operation); + Set expectedCookies = new HashSet<>(); + for (Entry entry : this.descriptorsByName.entrySet()) { + if (!entry.getValue().isOptional()) { + expectedCookies.add(entry.getKey()); + } + } + Set undocumentedCookies; + if (this.ignoreUndocumentedCookies) { + undocumentedCookies = Collections.emptySet(); + } + else { + undocumentedCookies = new HashSet<>(actualCookies); + undocumentedCookies.removeAll(this.descriptorsByName.keySet()); + } + Set missingCookies = new HashSet<>(expectedCookies); + missingCookies.removeAll(actualCookies); + + if (!undocumentedCookies.isEmpty() || !missingCookies.isEmpty()) { + verificationFailed(undocumentedCookies, missingCookies); + } + } + + /** + * Extracts the names of the cookies from the request or response of the given + * {@code operation}. + * @param operation the operation + * @return the cookie names + */ + protected abstract Set extractActualCookies(Operation operation); + + /** + * Called when the documented cookies do not match the actual cookies. + * @param undocumentedCookies the cookies that were found in the operation but were + * not documented + * @param missingCookies the cookies that were documented but were not found in the + * operation + */ + protected abstract void verificationFailed(Set undocumentedCookies, Set missingCookies); + + /** + * Returns the list of {@link CookieDescriptor CookieDescriptors} that will be used to + * generate the documentation. + * @return the cookie descriptors + */ + protected final Map getCookieDescriptors() { + return this.descriptorsByName; + } + + /** + * Returns whether or not this snippet ignores undocumented cookies. + * @return {@code true} if undocumented cookies are ignored, otherwise {@code false} + */ + protected final boolean isIgnoreUndocumentedCookies() { + return this.ignoreUndocumentedCookies; + } + + /** + * Returns a model for the given {@code descriptor}. + * @param descriptor the descriptor + * @return the model + */ + protected Map createModelForDescriptor(CookieDescriptor descriptor) { + Map model = new HashMap<>(); + model.put("name", descriptor.getName()); + model.put("description", descriptor.getDescription()); + model.put("optional", descriptor.isOptional()); + model.putAll(descriptor.getAttributes()); + return model; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java new file mode 100644 index 000000000..97ee508c8 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package org.springframework.restdocs.cookies; + +import org.springframework.restdocs.snippet.IgnorableDescriptor; + +/** + * A description of a cookie found in a request or response. + * + * @author Clyde Stubbs + * @author Andy Wilkinson + * @since 3.0 + * @see CookieDocumentation#cookieWithName(String) + */ +public class CookieDescriptor extends IgnorableDescriptor { + + private final String name; + + private boolean optional; + + /** + * Creates a new {@code CookieDescriptor} describing the cookie with the given + * {@code name}. + * @param name the name + */ + protected CookieDescriptor(String name) { + this.name = name; + } + + /** + * Marks the cookie as optional. + * @return {@code this} + */ + public final CookieDescriptor optional() { + this.optional = true; + return this; + } + + /** + * Returns the name for the cookie. + * @return the cookie name + */ + public final String getName() { + return this.name; + } + + /** + * Returns {@code true} if the described cookie is optional, otherwise {@code false}. + * @return {@code true} if the described cookie is optional, otherwise {@code false} + */ + public final boolean isOptional() { + return this.optional; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java new file mode 100644 index 000000000..20c20f43f --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java @@ -0,0 +1,316 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package org.springframework.restdocs.cookies; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting a RESTful API's request and response cookies. + * + * @author Clyde Stubbs + * @author Andy Wilkinson + * @since 3.0 + */ +public abstract class CookieDocumentation { + + private CookieDocumentation() { + + } + + /** + * Creates a {@code CookieDescriptor} that describes a cookie with the given + * {@code name}. + * @param name the name of the cookie + * @return a {@code CookieDescriptor} ready for further configuration + */ + public static CookieDescriptor cookieWithName(String name) { + return new CookieDescriptor(name); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * request. The cookies will be documented using the given {@code descriptors}. + *

          + * If a cookie is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet requestCookies(CookieDescriptor... descriptors) { + return requestCookies(Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * request. The cookies will be documented using the given {@code descriptors}. + *

          + * If a cookie is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet requestCookies(List descriptors) { + return new RequestCookiesSnippet(descriptors); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * request. The cookies will be documented using the given {@code descriptors}. + *

          + * If a cookie is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented cookies will be ignored. + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet relaxedRequestCookies(CookieDescriptor... descriptors) { + return relaxedRequestCookies(Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * request. The cookies will be documented using the given {@code descriptors}. + *

          + * If a cookie is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented cookies will be ignored. + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet relaxedRequestCookies(List descriptors) { + return new RequestCookiesSnippet(descriptors, true); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the cookies will be documented using the given {@code descriptors}. + *

          + * If a cookie is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. + * @param attributes the attributes + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet requestCookies(Map attributes, + CookieDescriptor... descriptors) { + return requestCookies(attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the cookies will be documented using the given {@code descriptors}. + *

          + * If a cookie is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. + * @param attributes the attributes + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet requestCookies(Map attributes, + List descriptors) { + return new RequestCookiesSnippet(descriptors, attributes); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the cookies will be documented using the given {@code descriptors}. + *

          + * If a cookie is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented cookies will be ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet relaxedRequestCookies(Map attributes, + CookieDescriptor... descriptors) { + return relaxedRequestCookies(attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the cookies will be documented using the given {@code descriptors}. + *

          + * If a cookie is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented cookies will be ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the request's cookies + * @return the snippet that will document the request cookies + * @see #cookieWithName(String) + */ + public static RequestCookiesSnippet relaxedRequestCookies(Map attributes, + List descriptors) { + return new RequestCookiesSnippet(descriptors, attributes, true); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * response. The cookies will be documented using the given {@code descriptors}. + *

          + * If a cookie is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the + * response, a failure will also occur. + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet responseCookies(CookieDescriptor... descriptors) { + return responseCookies(Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * response. The cookies will be documented using the given {@code descriptors}. + *

          + * If a cookie is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the + * response, a failure will also occur. + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet responseCookies(List descriptors) { + return new ResponseCookiesSnippet(descriptors); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * response. The cookies will be documented using the given {@code descriptors}. + *

          + * If a cookie is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented cookies will be ignored. + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet relaxedResponseCookies(CookieDescriptor... descriptors) { + return relaxedResponseCookies(Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API operation's + * response. The cookies will be documented using the given {@code descriptors}. + *

          + * If a cookie is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented cookies will be ignored. + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet relaxedResponseCookies(List descriptors) { + return new ResponseCookiesSnippet(descriptors, true); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the cookies will be documented using the given + * {@code descriptors}. + *

          + * If a cookie is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the + * response, a failure will also occur. + * @param attributes the attributes + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet responseCookies(Map attributes, + CookieDescriptor... descriptors) { + return responseCookies(attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the cookies will be documented using the given + * {@code descriptors}. + *

          + * If a cookie is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * cookie is documented, is not marked as optional, and is not present in the + * response, a failure will also occur. + * @param attributes the attributes + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet responseCookies(Map attributes, + List descriptors) { + return new ResponseCookiesSnippet(descriptors, attributes); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the cookies will be documented using the given + * {@code descriptors}. + *

          + * If a cookie is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented cookies will be ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet relaxedResponseCookies(Map attributes, + CookieDescriptor... descriptors) { + return relaxedResponseCookies(attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a new {@link Snippet} that will document the cookies of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the cookies will be documented using the given + * {@code descriptors}. + *

          + * If a cookie is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented cookies will be ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the response's cookies + * @return the snippet that will document the response cookies + * @see #cookieWithName(String) + */ + public static ResponseCookiesSnippet relaxedResponseCookies(Map attributes, + List descriptors) { + return new ResponseCookiesSnippet(descriptors, attributes, true); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java new file mode 100644 index 000000000..e5f4d5d2a --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java @@ -0,0 +1,136 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package org.springframework.restdocs.cookies; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.RequestCookie; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; + +/** + * A {@link Snippet} that documents the cookies in a request. + * + * @author Clyde Stubbs + * @author Andy Wilkinson + * @since 3.0 + * @see CookieDocumentation#requestCookies(CookieDescriptor...) + * @see CookieDocumentation#requestCookies(Map, CookieDescriptor...) + */ +public class RequestCookiesSnippet extends AbstractCookiesSnippet { + + /** + * Creates a new {@code RequestCookiesSnippet} that will document the cookies in the + * request using the given {@code descriptors}. + * @param descriptors the descriptors + */ + protected RequestCookiesSnippet(List descriptors) { + this(descriptors, null, false); + } + + /** + * Creates a new {@code RequestCookiesSnippet} that will document the cookies in the + * request using the given {@code descriptors}. If {@code ignoreUndocumentedCookies} + * is {@code true}, undocumented cookies will be ignored and will not trigger a + * failure. + * @param descriptors the descriptors + * @param ignoreUndocumentedCookies whether undocumented cookies should be ignored + */ + protected RequestCookiesSnippet(List descriptors, boolean ignoreUndocumentedCookies) { + this(descriptors, null, ignoreUndocumentedCookies); + } + + /** + * Creates a new {@code RequestCookiesSnippet} that will document the cookies in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. Undocumented cookies will not be + * ignored. + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestCookiesSnippet(List descriptors, Map attributes) { + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestCookiesSnippet} that will document the cookies in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedCookies whether undocumented cookies should be ignored + */ + protected RequestCookiesSnippet(List descriptors, Map attributes, + boolean ignoreUndocumentedCookies) { + super("request", descriptors, attributes, ignoreUndocumentedCookies); + } + + @Override + protected Set extractActualCookies(Operation operation) { + HashSet actualCookies = new HashSet<>(); + for (RequestCookie cookie : operation.getRequest().getCookies()) { + actualCookies.add(cookie.getName()); + } + return actualCookies; + } + + @Override + protected void verificationFailed(Set undocumentedCookies, Set missingCookies) { + String message = ""; + if (!undocumentedCookies.isEmpty()) { + message += "Cookies with the following names were not documented: " + undocumentedCookies; + } + if (!missingCookies.isEmpty()) { + if (message.length() > 0) { + message += ". "; + } + message += "Cookies with the following names were not found in the request: " + missingCookies; + } + throw new SnippetException(message); + } + + /** + * Returns a new {@code RequestCookiesSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestCookiesSnippet and(CookieDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code RequestCookiesSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestCookiesSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(this.getCookieDescriptors().values()); + combinedDescriptors.addAll(additionalDescriptors); + return new RequestCookiesSnippet(combinedDescriptors, getAttributes(), isIgnoreUndocumentedCookies()); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java new file mode 100644 index 000000000..f6a5176f9 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java @@ -0,0 +1,132 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package org.springframework.restdocs.cookies; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.ResponseCookie; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; + +/** + * A {@link Snippet} that documents the cookies in a response. + * + * @author Clyde Stubbs + * @author Andy Wilkinson + * @since 3.0 + * @see CookieDocumentation#responseCookies(CookieDescriptor...) + * @see CookieDocumentation#responseCookies(Map, CookieDescriptor...) + */ +public class ResponseCookiesSnippet extends AbstractCookiesSnippet { + + /** + * Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the + * response using the given {@code descriptors}. + * @param descriptors the descriptors + */ + protected ResponseCookiesSnippet(List descriptors) { + this(descriptors, null, false); + } + + /** + * Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the + * response using the given {@code descriptors}. If {@code ignoreUndocumentedCookies} + * is {@code true}, undocumented cookies will be ignored and will not trigger a + * failure. + * @param descriptors the descriptors + * @param ignoreUndocumentedCookies whether undocumented cookies should be ignored + */ + protected ResponseCookiesSnippet(List descriptors, boolean ignoreUndocumentedCookies) { + this(descriptors, null, ignoreUndocumentedCookies); + } + + /** + * Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the + * response using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. Undocumented cookies will not be + * ignored. + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected ResponseCookiesSnippet(List descriptors, Map attributes) { + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the + * response using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedCookies whether undocumented cookies should be ignored + */ + protected ResponseCookiesSnippet(List descriptors, Map attributes, + boolean ignoreUndocumentedCookies) { + super("response", descriptors, attributes, ignoreUndocumentedCookies); + } + + @Override + protected Set extractActualCookies(Operation operation) { + return operation.getResponse().getCookies().stream().map(ResponseCookie::getName).collect(Collectors.toSet()); + } + + @Override + protected void verificationFailed(Set undocumentedCookies, Set missingCookies) { + String message = ""; + if (!undocumentedCookies.isEmpty()) { + message += "Cookies with the following names were not documented: " + undocumentedCookies; + } + if (!missingCookies.isEmpty()) { + if (message.length() > 0) { + message += ". "; + } + message += "Cookies with the following names were not found in the response: " + missingCookies; + } + throw new SnippetException(message); + } + + /** + * Returns a new {@code ResponseCookiesSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final ResponseCookiesSnippet and(CookieDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code ResponseCookiesSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final ResponseCookiesSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(this.getCookieDescriptors().values()); + combinedDescriptors.addAll(additionalDescriptors); + return new ResponseCookiesSnippet(combinedDescriptors, getAttributes(), isIgnoreUndocumentedCookies()); + } + +} diff --git a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java similarity index 73% rename from samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java index 789767500..afc01d60f 100644 --- a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,7 @@ * limitations under the License. */ -class UrlMappings { - - static mappings = { - "/"(controller: 'index') - "500"(controller: 'InternalServerError') - "404"(controller: 'NotFound') - } - -} +/** + * Documenting the cookies of a RESTful API's requests and responses. + */ +package org.springframework.restdocs.cookies; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java deleted file mode 100644 index d47227923..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.curl; - -import java.util.Map; - -import org.springframework.restdocs.snippet.Snippet; - -/** - * Static factory methods for documenting a RESTful API as if it were being driven using - * the cURL command-line utility. - * - * @deprecated Since 1.1 in favor of - * {@link org.springframework.restdocs.cli.CliDocumentation}. - * @author Andy Wilkinson - * @author Yann Le Guern - * @author Dmitriy Mayboroda - * @author Jonathan Pearlin - */ -@Deprecated -public abstract class CurlDocumentation { - - private CurlDocumentation() { - - } - - /** - * Returns a new {@code Snippet} that will document the curl request for the API - * operation. - * - * @return the snippet that will document the curl request - * - * @deprecated Since 1.1 in favor of - * {@link org.springframework.restdocs.cli.CliDocumentation#curlRequest()}. - */ - @Deprecated - public static Snippet curlRequest() { - return new CurlRequestSnippet(); - } - - /** - * Returns a new {@code Snippet} that will document the curl request for the API - * operation. The given {@code attributes} will be available during snippet - * generation. - * - * @param attributes the attributes - * @return the snippet that will document the curl request - * - * @deprecated Since 1.1 in favor of - * {@link org.springframework.restdocs.cli.CliDocumentation#curlRequest(Map)}. - */ - @Deprecated - public static Snippet curlRequest(Map attributes) { - return new CurlRequestSnippet(attributes); - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java deleted file mode 100644 index a9fd0f28a..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.curl; - -import java.util.Map; - -import org.springframework.restdocs.snippet.Snippet; - -/** - * A {@link Snippet} that documents the curl command for a request. - * - * @author Andy Wilkinson - * @author Paul-Christian Volkmer - * @author Raman Gupta - * @deprecated Since 1.1 in favor of - * {@link org.springframework.restdocs.cli.CurlRequestSnippet}. - */ -@Deprecated -public class CurlRequestSnippet - extends org.springframework.restdocs.cli.CurlRequestSnippet { - - /** - * Creates a new {@code CurlRequestSnippet} with no additional attributes. - * - * @deprecated Since 1.1 in favor of - * {@link org.springframework.restdocs.cli.CurlRequestSnippet}. - */ - @Deprecated - protected CurlRequestSnippet() { - super(); - } - - /** - * Creates a new {@code CurlRequestSnippet} with additional attributes. - * @param attributes The additional attributes. - * - * @deprecated Since 1.1 in favor of - * {@link org.springframework.restdocs.cli.CurlRequestSnippet}. - */ - @Deprecated - protected CurlRequestSnippet(final Map attributes) { - super(attributes); - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java deleted file mode 100644 index b1bb7454e..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -/** - * Documenting the curl command required to make a request to a RESTful API. - * - * @deprecated Since 1.1 in favor of functionality in - * {@code org.springframework.restdocs.cli} - */ -package org.springframework.restdocs.curl; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java index e86d144c7..b12a1a9cd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java @@ -27,7 +27,6 @@ public class RestDocumentationGenerationException extends RuntimeException { /** * Creates a new {@code RestDocumentationException} with the given {@code cause}. - * * @param cause the cause */ public RestDocumentationGenerationException(Throwable cause) { @@ -37,7 +36,6 @@ public RestDocumentationGenerationException(Throwable cause) { /** * Creates a new {@code RestDocumentationException} with the given {@code message} and * {@code cause}. - * * @param message the message * @param cause the cause */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java index 2dece9f2b..04e64bbb9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; @@ -41,6 +42,7 @@ * @param the request type that can be handled * @param the response type that can be handled * @author Andy Wilkinson + * @author Filip Hrisafov * @since 1.1.0 */ public final class RestDocumentationGenerator { @@ -55,6 +57,18 @@ public final class RestDocumentationGenerator { */ public static final String ATTRIBUTE_NAME_DEFAULT_SNIPPETS = "org.springframework.restdocs.defaultSnippets"; + /** + * Name of the operation attribute used to hold the default operation request + * preprocessor. + */ + public static final String ATTRIBUTE_NAME_DEFAULT_OPERATION_REQUEST_PREPROCESSOR = "org.springframework.restdocs.defaultOperationRequestPreprocessor"; + + /** + * Name of the operation attribute used to hold the default operation response + * preprocessor. + */ + public static final String ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR = "org.springframework.restdocs.defaultOperationResponsePreprocessor"; + private final String identifier; private final OperationRequestPreprocessor requestPreprocessor; @@ -63,8 +77,6 @@ public final class RestDocumentationGenerator { private final List snippets; - private final List additionalSnippets; - private final RequestConverter requestConverter; private final ResponseConverter responseConverter; @@ -75,17 +87,14 @@ public final class RestDocumentationGenerator { * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that * can then be documented. The given documentation {@code snippets} will be produced. - * * @param identifier the identifier for the operation * @param requestConverter the request converter * @param responseConverter the response converter * @param snippets the snippets */ - public RestDocumentationGenerator(String identifier, - RequestConverter requestConverter, + public RestDocumentationGenerator(String identifier, RequestConverter requestConverter, ResponseConverter responseConverter, Snippet... snippets) { - this(identifier, requestConverter, responseConverter, - new IdentityOperationRequestPreprocessor(), + this(identifier, requestConverter, responseConverter, new IdentityOperationRequestPreprocessor(), new IdentityOperationResponsePreprocessor(), snippets); } @@ -97,17 +106,15 @@ public RestDocumentationGenerator(String identifier, * can then be documented. The given {@code requestPreprocessor} is applied to the * request before it is documented. The given documentation {@code snippets} will be * produced. - * * @param identifier the identifier for the operation * @param requestConverter the request converter * @param responseConverter the response converter * @param requestPreprocessor the request preprocessor * @param snippets the snippets */ - public RestDocumentationGenerator(String identifier, - RequestConverter requestConverter, - ResponseConverter responseConverter, - OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { + public RestDocumentationGenerator(String identifier, RequestConverter requestConverter, + ResponseConverter responseConverter, OperationRequestPreprocessor requestPreprocessor, + Snippet... snippets) { this(identifier, requestConverter, responseConverter, requestPreprocessor, new IdentityOperationResponsePreprocessor(), snippets); } @@ -120,20 +127,17 @@ public RestDocumentationGenerator(String identifier, * can then be documented. The given {@code responsePreprocessor} is applied to the * response before it is documented. The given documentation {@code snippets} will be * produced. - * * @param identifier the identifier for the operation * @param requestConverter the request converter * @param responseConverter the response converter * @param responsePreprocessor the response preprocessor * @param snippets the snippets */ - public RestDocumentationGenerator(String identifier, - RequestConverter requestConverter, - ResponseConverter responseConverter, - OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - this(identifier, requestConverter, responseConverter, - new IdentityOperationRequestPreprocessor(), responsePreprocessor, - snippets); + public RestDocumentationGenerator(String identifier, RequestConverter requestConverter, + ResponseConverter responseConverter, OperationResponsePreprocessor responsePreprocessor, + Snippet... snippets) { + this(identifier, requestConverter, responseConverter, new IdentityOperationRequestPreprocessor(), + responsePreprocessor, snippets); } /** @@ -144,7 +148,6 @@ public RestDocumentationGenerator(String identifier, * can then be documented. The given {@code requestPreprocessor} and * {@code responsePreprocessor} are applied to the request and response before they * are documented. The given documentation {@code snippets} will be produced. - * * @param identifier the identifier for the operation * @param requestConverter the request converter * @param responseConverter the response converter @@ -152,10 +155,8 @@ public RestDocumentationGenerator(String identifier, * @param responsePreprocessor the response preprocessor * @param snippets the snippets */ - public RestDocumentationGenerator(String identifier, - RequestConverter requestConverter, - ResponseConverter responseConverter, - OperationRequestPreprocessor requestPreprocessor, + public RestDocumentationGenerator(String identifier, RequestConverter requestConverter, + ResponseConverter responseConverter, OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { Assert.notNull(identifier, "identifier must be non-null"); Assert.notNull(requestConverter, "requestConverter must be non-null"); @@ -170,27 +171,21 @@ public RestDocumentationGenerator(String identifier, this.requestPreprocessor = requestPreprocessor; this.responsePreprocessor = responsePreprocessor; this.snippets = new ArrayList<>(Arrays.asList(snippets)); - this.additionalSnippets = new ArrayList<>(); } /** * Handles the given {@code request} and {@code response}, producing documentation * snippets for them using the given {@code configuration}. - * * @param request the request * @param response the request * @param configuration the configuration * @throws RestDocumentationGenerationException if a failure occurs during handling */ public void handle(REQ request, RESP response, Map configuration) { - OperationRequest operationRequest = this.requestPreprocessor - .preprocess(this.requestConverter.convert(request)); - - OperationResponse operationResponse = this.responsePreprocessor - .preprocess(this.responseConverter.convert(response)); Map attributes = new HashMap<>(configuration); - Operation operation = new StandardOperation(this.identifier, operationRequest, - operationResponse, attributes); + OperationRequest operationRequest = preprocessRequest(this.requestConverter.convert(request), attributes); + OperationResponse operationResponse = preprocessResponse(this.responseConverter.convert(response), attributes); + Operation operation = new StandardOperation(this.identifier, operationRequest, operationResponse, attributes); try { for (Snippet snippet : getSnippets(attributes)) { snippet.document(operation); @@ -201,47 +196,76 @@ public void handle(REQ request, RESP response, Map configuration } } - /** - * Adds the given {@code snippets} such that they are documented when this handler is - * called. - * - * @param snippets the snippets to add - * @deprecated since 1.1 in favor of {@link #withSnippets(Snippet...)} - */ - @Deprecated - public void addSnippets(Snippet... snippets) { - this.additionalSnippets.addAll(Arrays.asList(snippets)); - } - /** * Creates a new {@link RestDocumentationGenerator} with the same configuration as * this one other than its snippets. The new generator will use the given * {@code snippets}. - * * @param snippets the snippets * @return the new generator */ public RestDocumentationGenerator withSnippets(Snippet... snippets) { - return new RestDocumentationGenerator<>(this.identifier, this.requestConverter, - this.responseConverter, this.requestPreprocessor, - this.responsePreprocessor, snippets); + return new RestDocumentationGenerator<>(this.identifier, this.requestConverter, this.responseConverter, + this.requestPreprocessor, this.responsePreprocessor, snippets); } @SuppressWarnings("unchecked") private List getSnippets(Map configuration) { - List combinedSnippets = new ArrayList<>(this.snippets); - List defaultSnippets = (List) configuration - .get(ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + List combinedSnippets = new ArrayList<>(); + List defaultSnippets = (List) configuration.get(ATTRIBUTE_NAME_DEFAULT_SNIPPETS); if (defaultSnippets != null) { combinedSnippets.addAll(defaultSnippets); } - combinedSnippets.addAll(this.additionalSnippets); - this.additionalSnippets.clear(); + combinedSnippets.addAll(this.snippets); return combinedSnippets; } - private static final class IdentityOperationRequestPreprocessor - implements OperationRequestPreprocessor { + private OperationRequest preprocessRequest(OperationRequest request, Map configuration) { + return preprocess(getRequestPreprocessors(configuration), request, this::preprocess); + } + + private OperationRequest preprocess(OperationRequestPreprocessor preprocessor, OperationRequest request) { + return preprocessor.preprocess(request); + } + + private List getRequestPreprocessors(Map configuration) { + return getPreprocessors(this.requestPreprocessor, + RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_REQUEST_PREPROCESSOR, configuration); + } + + private OperationResponse preprocessResponse(OperationResponse response, Map configuration) { + return preprocess(getResponsePreprocessors(configuration), response, this::preprocess); + } + + private OperationResponse preprocess(OperationResponsePreprocessor preprocessor, OperationResponse response) { + return preprocessor.preprocess(response); + } + + private List getResponsePreprocessors(Map configuration) { + return getPreprocessors(this.responsePreprocessor, + RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR, configuration); + } + + private T preprocess(List

          preprocessors, T target, BiFunction function) { + T processed = target; + for (P preprocessor : preprocessors) { + processed = function.apply(preprocessor, processed); + } + return processed; + } + + @SuppressWarnings("unchecked") + private List getPreprocessors(T preprocessor, String preprocessorAttribute, + Map configuration) { + List preprocessors = new ArrayList<>(2); + preprocessors.add(preprocessor); + T defaultResponsePreprocessor = (T) configuration.get(preprocessorAttribute); + if (defaultResponsePreprocessor != null) { + preprocessors.add(defaultResponsePreprocessor); + } + return preprocessors; + } + + private static final class IdentityOperationRequestPreprocessor implements OperationRequestPreprocessor { @Override public OperationRequest preprocess(OperationRequest request) { @@ -250,8 +274,7 @@ public OperationRequest preprocess(OperationRequest request) { } - private static final class IdentityOperationResponsePreprocessor - implements OperationResponsePreprocessor { + private static final class IdentityOperationResponsePreprocessor implements OperationResponsePreprocessor { @Override public OperationResponse preprocess(OperationResponse response) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java index 95f5ed7ea..6e049b7a7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,17 +44,15 @@ public abstract class AbstractHeadersSnippet extends TemplatedSnippet { * {@code -headers}. The headers will be documented using the given * {@code descriptors} and the given {@code attributes} will be included in the model * during template rendering. - * * @param type the type of the headers * @param descriptors the header descriptors * @param attributes the additional attributes */ - protected AbstractHeadersSnippet(String type, List descriptors, - Map attributes) { + protected AbstractHeadersSnippet(String type, List descriptors, Map attributes) { super(type + "-headers", attributes); for (HeaderDescriptor descriptor : descriptors) { - Assert.notNull(descriptor.getName()); - Assert.notNull(descriptor.getDescription()); + Assert.notNull(descriptor.getName(), "The name of the header must not be null"); + Assert.notNull(descriptor.getDescription(), "The description of the header must not be null"); } this.headerDescriptors = descriptors; this.type = type; @@ -80,8 +78,8 @@ private void validateHeaderDocumentation(Operation operation) { for (HeaderDescriptor headerDescriptor : missingHeaders) { names.add(headerDescriptor.getName()); } - throw new SnippetException("Headers with the following names were not found" - + " in the " + this.type + ": " + names); + throw new SnippetException( + "Headers with the following names were not found" + " in the " + this.type + ": " + names); } } @@ -89,7 +87,6 @@ private void validateHeaderDocumentation(Operation operation) { * Finds the headers that are missing from the operation. A header is missing if it is * described by one of the {@code headerDescriptors} but is not present in the * operation. - * * @param operation the operation * @return descriptors for the headers that are missing from the operation */ @@ -97,8 +94,7 @@ protected List findMissingHeaders(Operation operation) { List missingHeaders = new ArrayList<>(); Set actualHeaders = extractActualHeaders(operation); for (HeaderDescriptor headerDescriptor : this.headerDescriptors) { - if (!headerDescriptor.isOptional() - && !actualHeaders.contains(headerDescriptor.getName())) { + if (!headerDescriptor.isOptional() && !actualHeaders.contains(headerDescriptor.getName())) { missingHeaders.add(headerDescriptor); } } @@ -109,7 +105,6 @@ protected List findMissingHeaders(Operation operation) { /** * Extracts the names of the headers from the request or response of the given * {@code operation}. - * * @param operation the operation * @return the header names */ @@ -118,7 +113,6 @@ protected List findMissingHeaders(Operation operation) { /** * Returns the list of {@link HeaderDescriptor HeaderDescriptors} that will be used to * generate the documentation. - * * @return the header descriptors */ protected final List getHeaderDescriptors() { @@ -127,7 +121,6 @@ protected final List getHeaderDescriptors() { /** * Returns a model for the given {@code descriptor}. - * * @param descriptor the descriptor * @return the model */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java index 0719d3464..121446239 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java @@ -43,7 +43,6 @@ protected HeaderDescriptor(String name) { /** * Marks the header as optional. - * * @return {@code this} */ public final HeaderDescriptor optional() { @@ -53,7 +52,6 @@ public final HeaderDescriptor optional() { /** * Returns the name for the header. - * * @return the header name */ public final String getName() { @@ -62,7 +60,6 @@ public final String getName() { /** * Returns {@code true} if the described header is optional, otherwise {@code false}. - * * @return {@code true} if the described header is optional, otherwise {@code false} */ public final boolean isOptional() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java index 4ee15e45c..e3a090f0c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,8 +38,7 @@ private HeaderDocumentation() { /** * Creates a {@code HeaderDescriptor} that describes a header with the given * {@code name}. - * - * @param name The name of the header + * @param name the name of the header * @return a {@code HeaderDescriptor} ready for further configuration */ public static HeaderDescriptor headerWithName(String name) { @@ -52,7 +51,6 @@ public static HeaderDescriptor headerWithName(String name) { *

          * If a header is documented, is not marked as optional, and is not present in the * request, a failure will occur. - * * @param descriptors the descriptions of the request's headers * @return the snippet that will document the request headers * @see #headerWithName(String) @@ -67,13 +65,11 @@ public static RequestHeadersSnippet requestHeaders(HeaderDescriptor... descripto *

          * If a header is documented, is not marked as optional, and is not present in the * request, a failure will occur. - * * @param descriptors the descriptions of the request's headers * @return the snippet that will document the request headers * @see #headerWithName(String) */ - public static RequestHeadersSnippet requestHeaders( - List descriptors) { + public static RequestHeadersSnippet requestHeaders(List descriptors) { return new RequestHeadersSnippet(descriptors); } @@ -84,7 +80,6 @@ public static RequestHeadersSnippet requestHeaders( *

          * If a header is documented, is not marked as optional, and is not present in the * request, a failure will occur. - * * @param attributes the attributes * @param descriptors the descriptions of the request's headers * @return the snippet that will document the request headers @@ -102,7 +97,6 @@ public static RequestHeadersSnippet requestHeaders(Map attribute *

          * If a header is documented, is not marked as optional, and is not present in the * request, a failure will occur. - * * @param attributes the attributes * @param descriptors the descriptions of the request's headers * @return the snippet that will document the request headers @@ -119,13 +113,11 @@ public static RequestHeadersSnippet requestHeaders(Map attribute *

          * If a header is documented, is not marked as optional, and is not present in the * request, a failure will occur. - * * @param descriptors the descriptions of the response's headers * @return the snippet that will document the response headers * @see #headerWithName(String) */ - public static ResponseHeadersSnippet responseHeaders( - HeaderDescriptor... descriptors) { + public static ResponseHeadersSnippet responseHeaders(HeaderDescriptor... descriptors) { return responseHeaders(Arrays.asList(descriptors)); } @@ -135,13 +127,11 @@ public static ResponseHeadersSnippet responseHeaders( *

          * If a header is documented, is not marked as optional, and is not present in the * request, a failure will occur. - * * @param descriptors the descriptions of the response's headers * @return the snippet that will document the response headers * @see #headerWithName(String) */ - public static ResponseHeadersSnippet responseHeaders( - List descriptors) { + public static ResponseHeadersSnippet responseHeaders(List descriptors) { return new ResponseHeadersSnippet(descriptors); } @@ -153,7 +143,6 @@ public static ResponseHeadersSnippet responseHeaders( *

          * If a header is documented, is not marked as optional, and is not present in the * response, a failure will occur. - * * @param attributes the attributes * @param descriptors the descriptions of the response's headers * @return the snippet that will document the response headers @@ -172,7 +161,6 @@ public static ResponseHeadersSnippet responseHeaders(Map attribu *

          * If a header is documented, is not marked as optional, and is not present in the * response, a failure will occur. - * * @param attributes the attributes * @param descriptors the descriptions of the response's headers * @return the snippet that will document the response headers diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java index b301a5aac..79d587928 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,6 @@ public class RequestHeadersSnippet extends AbstractHeadersSnippet { /** * Creates a new {@code RequestHeadersSnippet} that will document the headers in the * request using the given {@code descriptors}. - * * @param descriptors the descriptors */ protected RequestHeadersSnippet(List descriptors) { @@ -49,18 +48,16 @@ protected RequestHeadersSnippet(List descriptors) { * Creates a new {@code RequestHeadersSnippet} that will document the headers in the * request using the given {@code descriptors}. The given {@code attributes} will be * included in the model during template rendering. - * * @param descriptors the descriptors * @param attributes the additional attributes */ - protected RequestHeadersSnippet(List descriptors, - Map attributes) { + protected RequestHeadersSnippet(List descriptors, Map attributes) { super("request", descriptors, attributes); } @Override protected Set extractActualHeaders(Operation operation) { - return operation.getRequest().getHeaders().keySet(); + return operation.getRequest().getHeaders().headerNames(); } /** @@ -82,8 +79,7 @@ public final RequestHeadersSnippet and(HeaderDescriptor... additionalDescriptors * @return the new snippet */ public final RequestHeadersSnippet and(List additionalDescriptors) { - List combinedDescriptors = new ArrayList<>( - this.getHeaderDescriptors()); + List combinedDescriptors = new ArrayList<>(this.getHeaderDescriptors()); combinedDescriptors.addAll(additionalDescriptors); return new RequestHeadersSnippet(combinedDescriptors, getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java index cb191bf45..ea9687e52 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,6 @@ public class ResponseHeadersSnippet extends AbstractHeadersSnippet { /** * Creates a new {@code ResponseHeadersSnippet} that will document the headers in the * response using the given {@code descriptors}. - * * @param descriptors the descriptors */ protected ResponseHeadersSnippet(List descriptors) { @@ -49,18 +48,16 @@ protected ResponseHeadersSnippet(List descriptors) { * Creates a new {@code ResponseHeadersSnippet} that will document the headers in the * response using the given {@code descriptors}. The given {@code attributes} will be * included in the model during template rendering. - * * @param descriptors the descriptors * @param attributes the additional attributes */ - protected ResponseHeadersSnippet(List descriptors, - Map attributes) { + protected ResponseHeadersSnippet(List descriptors, Map attributes) { super("response", descriptors, attributes); } @Override protected Set extractActualHeaders(Operation operation) { - return operation.getResponse().getHeaders().keySet(); + return operation.getResponse().getHeaders().headerNames(); } /** @@ -81,10 +78,8 @@ public final ResponseHeadersSnippet and(HeaderDescriptor... additionalDescriptor * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public final ResponseHeadersSnippet and( - List additionalDescriptors) { - List combinedDescriptors = new ArrayList<>( - this.getHeaderDescriptors()); + public final ResponseHeadersSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(this.getHeaderDescriptors()); combinedDescriptors.addAll(additionalDescriptors); return new ResponseHeadersSnippet(combinedDescriptors, getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index 5e6b13daf..3fac7ffac 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -33,7 +33,6 @@ private HttpDocumentation() { /** * Returns a new {@code Snippet} that will document the HTTP request for the API * operation. - * * @return the snippet that will document the HTTP request */ public static HttpRequestSnippet httpRequest() { @@ -44,7 +43,6 @@ public static HttpRequestSnippet httpRequest() { * Returns a new {@code Snippet} that will document the HTTP request for the API * operation. The given {@code attributes} will be available during snippet * generation. - * * @param attributes the attributes * @return the snippet that will document the HTTP request */ @@ -55,7 +53,6 @@ public static HttpRequestSnippet httpRequest(Map attributes) { /** * Returns a {@code Snippet} that will document the HTTP response for the API * operation. - * * @return the snippet that will document the HTTP response */ public static HttpResponseSnippet httpResponse() { @@ -66,7 +63,6 @@ public static HttpResponseSnippet httpResponse() { * Returns a {@code Snippet} that will document the HTTP response for the API * operation. The given {@code attributes} will be available during snippet * generation. - * * @param attributes the attributes * @return the snippet that will document the HTTP response */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index c397ff0e1..87087b315 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; @@ -56,8 +56,7 @@ protected HttpRequestSnippet() { /** * Creates a new {@code HttpRequestSnippet} with the given additional * {@code attributes} that will be included in the model during template rendering. - * - * @param attributes The additional attributes + * @param attributes the additional attributes */ protected HttpRequestSnippet(Map attributes) { super("http-request", attributes); @@ -76,16 +75,6 @@ protected Map createModel(Operation operation) { private String getPath(OperationRequest request) { String path = request.getUri().getRawPath(); String queryString = request.getUri().getRawQuery(); - Parameters uniqueParameters = request.getParameters() - .getUniqueParameters(request.getUri()); - if (!uniqueParameters.isEmpty() && includeParametersInUri(request)) { - if (StringUtils.hasText(queryString)) { - queryString = queryString + "&" + uniqueParameters.toQueryString(); - } - else { - queryString = uniqueParameters.toQueryString(); - } - } if (StringUtils.hasText(queryString)) { path = path + "?" + queryString; } @@ -93,18 +82,19 @@ private String getPath(OperationRequest request) { } private boolean includeParametersInUri(OperationRequest request) { - return request.getMethod() == HttpMethod.GET || request.getContent().length > 0; + HttpMethod method = request.getMethod(); + return (method != HttpMethod.PUT && method != HttpMethod.POST && method != HttpMethod.PATCH) + || (request.getContent().length > 0 && !MediaType.APPLICATION_FORM_URLENCODED + .isCompatibleWith(request.getHeaders().getContentType())); } private List> getHeaders(OperationRequest request) { List> headers = new ArrayList<>(); - for (Entry> header : request.getHeaders().entrySet()) { + for (Entry> header : request.getHeaders().headerSet()) { for (String value : header.getValue()) { - if (HttpHeaders.CONTENT_TYPE.equals(header.getKey()) - && !request.getParts().isEmpty()) { - headers.add(header(header.getKey(), - String.format("%s; boundary=%s", value, MULTIPART_BOUNDARY))); + if (HttpHeaders.CONTENT_TYPE.equals(header.getKey()) && !request.getParts().isEmpty()) { + headers.add(header(header.getKey(), String.format("%s; boundary=%s", value, MULTIPART_BOUNDARY))); } else { headers.add(header(header.getKey(), value)); @@ -112,9 +102,17 @@ private List> getHeaders(OperationRequest request) { } } + + List cookies = new ArrayList<>(); + for (RequestCookie cookie : request.getCookies()) { + cookies.add(String.format("%s=%s", cookie.getName(), cookie.getValue())); + } + if (!cookies.isEmpty()) { + headers.add(header(HttpHeaders.COOKIE, String.join("; ", cookies))); + } + if (requiresFormEncodingContentTypeHeader(request)) { - headers.add(header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_FORM_URLENCODED_VALUE)); + headers.add(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)); } return headers; } @@ -126,41 +124,21 @@ private String getRequestBody(OperationRequest request) { if (StringUtils.hasText(content)) { writer.printf("%n%s", content); } - else if (isPutOrPost(request)) { - if (request.getParts().isEmpty()) { - String queryString = request.getParameters().toQueryString(); - if (StringUtils.hasText(queryString)) { - writer.println(); - writer.print(queryString); - } - } - else { + else if (isPutPostOrPatch(request)) { + if (!request.getParts().isEmpty()) { writeParts(request, writer); } } return httpRequest.toString(); } - private boolean isPutOrPost(OperationRequest request) { - return HttpMethod.PUT.equals(request.getMethod()) - || HttpMethod.POST.equals(request.getMethod()); + private boolean isPutPostOrPatch(OperationRequest request) { + return HttpMethod.PUT.equals(request.getMethod()) || HttpMethod.POST.equals(request.getMethod()) + || HttpMethod.PATCH.equals(request.getMethod()); } private void writeParts(OperationRequest request, PrintWriter writer) { writer.println(); - for (Entry> parameter : request.getParameters().entrySet()) { - if (parameter.getValue().isEmpty()) { - writePartBoundary(writer); - writePart(parameter.getKey(), "", null, writer); - } - else { - for (String value : parameter.getValue()) { - writePartBoundary(writer); - writePart(parameter.getKey(), value, null, writer); - writer.println(); - } - } - } for (OperationRequestPart part : request.getParts()) { writePartBoundary(writer); writePart(part, writer); @@ -174,13 +152,16 @@ private void writePartBoundary(PrintWriter writer) { } private void writePart(OperationRequestPart part, PrintWriter writer) { - writePart(part.getName(), part.getContentAsString(), + writePart(part.getName(), part.getContentAsString(), part.getSubmittedFileName(), part.getHeaders().getContentType(), writer); } - private void writePart(String name, String value, MediaType contentType, - PrintWriter writer) { - writer.printf("Content-Disposition: form-data; name=%s%n", name); + private void writePart(String name, String value, String filename, MediaType contentType, PrintWriter writer) { + writer.printf("Content-Disposition: form-data; name=%s", name); + if (StringUtils.hasText(filename)) { + writer.printf("; filename=%s", filename); + } + writer.printf("%n"); if (contentType != null) { writer.printf("Content-Type: %s%n", contentType); } @@ -193,9 +174,8 @@ private void writeMultipartEnd(PrintWriter writer) { } private boolean requiresFormEncodingContentTypeHeader(OperationRequest request) { - return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null - && isPutOrPost(request) && (!request.getParameters().isEmpty() - && !includeParametersInUri(request)); + return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null && isPutPostOrPatch(request) + && !includeParametersInUri(request); } private Map header(String name, String value) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java index 46807070a..919d312f9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.util.Map.Entry; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.snippet.Snippet; @@ -47,8 +48,7 @@ protected HttpResponseSnippet() { /** * Creates a new {@code HttpResponseSnippet} with the given additional * {@code attributes} that will be included in the model during template rendering. - * - * @param attributes The additional attributes + * @param attributes the additional attributes */ protected HttpResponseSnippet(Map attributes) { super("http-response", attributes); @@ -57,12 +57,12 @@ protected HttpResponseSnippet(Map attributes) { @Override protected Map createModel(Operation operation) { OperationResponse response = operation.getResponse(); - HttpStatus status = response.getStatus(); Map model = new HashMap<>(); model.put("responseBody", responseBody(response)); - model.put("statusCode", status.value()); - model.put("statusReason", status.getReasonPhrase()); model.put("headers", headers(response)); + HttpStatusCode status = response.getStatus(); + model.put("statusCode", status.value()); + model.put("statusReason", (status instanceof HttpStatus) ? ((HttpStatus) status).getReasonPhrase() : ""); return model; } @@ -73,7 +73,7 @@ private String responseBody(OperationResponse response) { private List> headers(OperationResponse response) { List> headers = new ArrayList<>(); - for (Entry> header : response.getHeaders().entrySet()) { + for (Entry> header : response.getHeaders().headerSet()) { List values = header.getValue(); for (String value : values) { headers.add(header(header.getKey(), value)); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java index a15d69fea..a019ef7e7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,10 +35,8 @@ abstract class AbstractJsonLinkExtractor implements LinkExtractor { @Override @SuppressWarnings("unchecked") - public Map> extractLinks(OperationResponse response) - throws IOException { - Map jsonContent = this.objectMapper - .readValue(response.getContent(), Map.class); + public Map> extractLinks(OperationResponse response) throws IOException { + Map jsonContent = this.objectMapper.readValue(response.getContent(), Map.class); return extractLinks(jsonContent); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java index a64bf2920..f2a68f897 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,13 +53,12 @@ private static Link maybeCreateLink(Map linkMap) { if (relObject instanceof String && hrefObject instanceof String) { Object titleObject = linkMap.get("title"); return new Link((String) relObject, (String) hrefObject, - titleObject instanceof String ? (String) titleObject : null); + (titleObject instanceof String) ? (String) titleObject : null); } return null; } - private static void maybeStoreLink(Link link, - MultiValueMap extractedLinks) { + private static void maybeStoreLink(Link link, MultiValueMap extractedLinks) { if (link != null) { extractedLinks.add(link.getRel(), link); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java index b1f0673fd..bdf5772cb 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ * content type. * * @author Andy Wilkinson - * + * @author Oliver Drotbohm */ class ContentTypeLinkExtractor implements LinkExtractor { @@ -38,7 +38,10 @@ class ContentTypeLinkExtractor implements LinkExtractor { ContentTypeLinkExtractor() { this.linkExtractors.put(MediaType.APPLICATION_JSON, new AtomLinkExtractor()); - this.linkExtractors.put(HalLinkExtractor.HAL_MEDIA_TYPE, new HalLinkExtractor()); + LinkExtractor halLinkExtractor = new HalLinkExtractor(); + this.linkExtractors.put(HalLinkExtractor.HAL_MEDIA_TYPE, halLinkExtractor); + this.linkExtractors.put(HalLinkExtractor.VND_HAL_MEDIA_TYPE, halLinkExtractor); + this.linkExtractors.put(HalLinkExtractor.HAL_FORMS_MEDIA_TYPE, halLinkExtractor); } ContentTypeLinkExtractor(Map linkExtractors) { @@ -46,16 +49,14 @@ class ContentTypeLinkExtractor implements LinkExtractor { } @Override - public Map> extractLinks(OperationResponse response) - throws IOException { + public Map> extractLinks(OperationResponse response) throws IOException { MediaType contentType = response.getHeaders().getContentType(); LinkExtractor extractorForContentType = getExtractorForContentType(contentType); if (extractorForContentType != null) { return extractorForContentType.extractLinks(response); } throw new IllegalStateException( - "No LinkExtractor has been provided and one is not available for the " - + "content type " + contentType); + "No LinkExtractor has been provided and one is not available for the " + "content type " + contentType); } private LinkExtractor getExtractorForContentType(MediaType contentType) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java index 5b3c82e99..e56f32ba8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,11 +30,16 @@ * format. * * @author Andy Wilkinson + * @author Oliver Drotbohm */ class HalLinkExtractor extends AbstractJsonLinkExtractor { static final MediaType HAL_MEDIA_TYPE = new MediaType("application", "hal+json"); + static final MediaType VND_HAL_MEDIA_TYPE = new MediaType("application", "vnd.hal+json"); + + static final MediaType HAL_FORMS_MEDIA_TYPE = new MediaType("application", "prs.hal-forms+json"); + @Override public Map> extractLinks(Map json) { Map> extractedLinks = new LinkedHashMap<>(); @@ -72,7 +77,7 @@ private static Link maybeCreateLink(String rel, Object possibleLinkObject) { if (hrefObject instanceof String) { Object titleObject = possibleLinkMap.get("title"); return new Link(rel, (String) hrefObject, - titleObject instanceof String ? (String) titleObject : null); + (titleObject instanceof String) ? (String) titleObject : null); } } return null; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index 635185090..74bacc616 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,8 +34,7 @@ private HypermediaDocumentation() { /** * Creates a {@code LinkDescriptor} that describes a link with the given {@code rel}. - * - * @param rel The rel of the link + * @param rel the rel of the link * @return a {@code LinkDescriptor} ready for further configuration */ public static LinkDescriptor linkWithRel(String rel) { @@ -59,7 +58,6 @@ public static LinkDescriptor linkWithRel(String rel) { * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ @@ -84,7 +82,6 @@ public static LinksSnippet links(LinkDescriptor... descriptors) { * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ @@ -103,7 +100,6 @@ public static LinksSnippet links(List descriptors) { * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ @@ -122,7 +118,6 @@ public static LinksSnippet relaxedLinks(LinkDescriptor... descriptors) { * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ @@ -148,13 +143,11 @@ public static LinksSnippet relaxedLinks(List descriptors) { * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param attributes the attributes * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static LinksSnippet links(Map attributes, - LinkDescriptor... descriptors) { + public static LinksSnippet links(Map attributes, LinkDescriptor... descriptors) { return links(attributes, Arrays.asList(descriptors)); } @@ -176,13 +169,11 @@ public static LinksSnippet links(Map attributes, * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param attributes the attributes * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static LinksSnippet links(Map attributes, - List descriptors) { + public static LinksSnippet links(Map attributes, List descriptors) { return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, attributes); } @@ -198,13 +189,11 @@ public static LinksSnippet links(Map attributes, * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param attributes the attributes * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static LinksSnippet relaxedLinks(Map attributes, - LinkDescriptor... descriptors) { + public static LinksSnippet relaxedLinks(Map attributes, LinkDescriptor... descriptors) { return relaxedLinks(attributes, Arrays.asList(descriptors)); } @@ -220,15 +209,12 @@ public static LinksSnippet relaxedLinks(Map attributes, * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param attributes the attributes * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static LinksSnippet relaxedLinks(Map attributes, - List descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, attributes, - true); + public static LinksSnippet relaxedLinks(Map attributes, List descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, attributes, true); } /** @@ -248,13 +234,11 @@ public static LinksSnippet relaxedLinks(Map attributes, * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static LinksSnippet links(LinkExtractor linkExtractor, - LinkDescriptor... descriptors) { + public static LinksSnippet links(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { return links(linkExtractor, Arrays.asList(descriptors)); } @@ -275,13 +259,11 @@ public static LinksSnippet links(LinkExtractor linkExtractor, * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static LinksSnippet links(LinkExtractor linkExtractor, - List descriptors) { + public static LinksSnippet links(LinkExtractor linkExtractor, List descriptors) { return new LinksSnippet(linkExtractor, descriptors); } @@ -296,13 +278,11 @@ public static LinksSnippet links(LinkExtractor linkExtractor, * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, - LinkDescriptor... descriptors) { + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { return relaxedLinks(linkExtractor, Arrays.asList(descriptors)); } @@ -317,13 +297,11 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, - List descriptors) { + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, List descriptors) { return new LinksSnippet(linkExtractor, descriptors, true); } @@ -345,14 +323,13 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param attributes the attributes * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static LinksSnippet links(LinkExtractor linkExtractor, - Map attributes, LinkDescriptor... descriptors) { + public static LinksSnippet links(LinkExtractor linkExtractor, Map attributes, + LinkDescriptor... descriptors) { return links(linkExtractor, attributes, Arrays.asList(descriptors)); } @@ -374,14 +351,13 @@ public static LinksSnippet links(LinkExtractor linkExtractor, * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param attributes the attributes * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static LinksSnippet links(LinkExtractor linkExtractor, - Map attributes, List descriptors) { + public static LinksSnippet links(LinkExtractor linkExtractor, Map attributes, + List descriptors) { return new LinksSnippet(linkExtractor, descriptors, attributes); } @@ -397,14 +373,13 @@ public static LinksSnippet links(LinkExtractor linkExtractor, * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param attributes the attributes * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, - Map attributes, LinkDescriptor... descriptors) { + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, Map attributes, + LinkDescriptor... descriptors) { return relaxedLinks(linkExtractor, attributes, Arrays.asList(descriptors)); } @@ -420,14 +395,13 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, * If a descriptor does not have a {@link LinkDescriptor#description(Object) * description}, the {@link Link#getTitle() title} of the link will be used. If the * link does not have a title a failure will occur. - * * @param attributes the attributes * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, - Map attributes, List descriptors) { + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, Map attributes, + List descriptors) { return new LinksSnippet(linkExtractor, descriptors, attributes, true); } @@ -445,8 +419,7 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, * } * } * - * - * @return The extractor for HAL-style links + * @return the extractor for HAL-style links */ public static LinkExtractor halLinks() { return new HalLinkExtractor(); @@ -466,10 +439,10 @@ public static LinkExtractor halLinks() { * ] * } * - * - * @return The extractor for Atom-style links + * @return the extractor for Atom-style links */ public static LinkExtractor atomLinks() { return new AtomLinkExtractor(); } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java index 94c477136..ace5f5eff 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,9 +33,8 @@ public class Link { /** * Creates a new {@code Link} with the given {@code rel} and {@code href}. - * - * @param rel The link's rel - * @param href The link's href + * @param rel the link's rel + * @param href the link's href */ public Link(String rel, String href) { this(rel, href, null); @@ -44,10 +43,9 @@ public Link(String rel, String href) { /** * Creates a new {@code Link} with the given {@code rel}, {@code href}, and * {@code title}. - * - * @param rel The link's rel - * @param href The link's href - * @param title The link's title + * @param rel the link's rel + * @param href the link's href + * @param title the link's title */ public Link(String rel, String href, String title) { this.rel = rel; @@ -79,16 +77,6 @@ public String getTitle() { return this.title; } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + this.href.hashCode(); - result = prime * result + this.rel.hashCode(); - result = prime * result + ((this.title == null) ? 0 : this.title.hashCode()); - return result; - } - @Override public boolean equals(Object obj) { if (this == obj) { @@ -118,10 +106,22 @@ else if (!this.title.equals(other.title)) { return true; } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.href.hashCode(); + result = prime * result + this.rel.hashCode(); + result = prime * result + ((this.title == null) ? 0 : this.title.hashCode()); + return result; + } + @Override public String toString() { - return new ToStringCreator(this).append("rel", this.rel).append("href", this.href) - .append("title", this.title).toString(); + return new ToStringCreator(this).append("rel", this.rel) + .append("href", this.href) + .append("title", this.title) + .toString(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java index 0b2cc7a01..9e69717e3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java @@ -32,7 +32,6 @@ public class LinkDescriptor extends IgnorableDescriptor { /** * Creates a new {@code LinkDescriptor} describing a link with the given {@code rel}. - * * @param rel the rel of the link */ protected LinkDescriptor(String rel) { @@ -41,7 +40,6 @@ protected LinkDescriptor(String rel) { /** * Marks the link as optional. - * * @return {@code this} */ public final LinkDescriptor optional() { @@ -51,7 +49,6 @@ public final LinkDescriptor optional() { /** * Returns the rel of the link described by this descriptor. - * * @return the rel */ public final String getRel() { @@ -60,7 +57,6 @@ public final String getRel() { /** * Returns {@code true} if the described link is optional, otherwise {@code false}. - * * @return {@code true} if the described link is optional, otherwise {@code false} */ public final boolean isOptional() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java index be05e0f24..ade697e70 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java @@ -34,9 +34,8 @@ public interface LinkExtractor { /** * Extract the links from the given {@code response}, returning a {@code Map} of links * where the keys are the link rels. - * - * @param response The response from which the links are to be extracted - * @return The extracted links, keyed by rel + * @param response the response from which the links are to be extracted + * @return the extracted links, keyed by rel * @throws IOException if link extraction fails */ Map> extractLinks(OperationResponse response) throws IOException; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index a30bfeaf0..e5ee2ce8c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,12 +57,10 @@ public class LinksSnippet extends TemplatedSnippet { * Creates a new {@code LinksSnippet} that will extract links using the given * {@code linkExtractor} and document them using the given {@code descriptors}. * Undocumented links will trigger a failure. - * * @param linkExtractor the link extractor * @param descriptors the link descriptors */ - protected LinksSnippet(LinkExtractor linkExtractor, - List descriptors) { + protected LinksSnippet(LinkExtractor linkExtractor, List descriptors) { this(linkExtractor, descriptors, null, false); } @@ -71,7 +69,6 @@ protected LinksSnippet(LinkExtractor linkExtractor, * {@code linkExtractor} and document them using the given {@code descriptors}. If * {@code ignoreUndocumentedLinks} is {@code true}, undocumented links will be ignored * and will not trigger a failure. - * * @param linkExtractor the link extractor * @param descriptors the link descriptors * @param ignoreUndocumentedLinks whether undocumented links should be ignored @@ -86,7 +83,6 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip * {@code linkExtractor} and document them using the given {@code descriptors}. The * given {@code attributes} will be included in the model during template rendering. * Undocumented links will trigger a failure. - * * @param linkExtractor the link extractor * @param descriptors the link descriptors * @param attributes the additional attributes @@ -102,7 +98,6 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip * given {@code attributes} will be included in the model during template rendering. * If {@code ignoreUndocumentedLinks} is {@code true}, undocumented links will be * ignored and will not trigger a failure. - * * @param linkExtractor the link extractor * @param descriptors the link descriptors * @param attributes the additional attributes @@ -148,8 +143,7 @@ private void validate(Map> links) { } Set requiredRels = new HashSet<>(); - for (Entry relAndDescriptor : this.descriptorsByRel - .entrySet()) { + for (Entry relAndDescriptor : this.descriptorsByRel.entrySet()) { if (!relAndDescriptor.getValue().isOptional()) { requiredRels.add(relAndDescriptor.getKey()); } @@ -161,15 +155,13 @@ private void validate(Map> links) { if (!undocumentedRels.isEmpty() || !missingRels.isEmpty()) { String message = ""; if (!undocumentedRels.isEmpty()) { - message += "Links with the following relations were not documented: " - + undocumentedRels; + message += "Links with the following relations were not documented: " + undocumentedRels; } if (!missingRels.isEmpty()) { if (message.length() > 0) { message += ". "; } - message += "Links with the following relations were not found in the " - + "response: " + missingRels; + message += "Links with the following relations were not found in the " + "response: " + missingRels; } throw new SnippetException(message); } @@ -181,9 +173,7 @@ private List> createLinksModel(Map> links LinkDescriptor descriptor = entry.getValue(); if (!descriptor.isIgnored()) { if (descriptor.getDescription() == null) { - descriptor = createDescriptor( - getDescriptionFromLinkTitle(links, descriptor.getRel()), - descriptor); + descriptor = createDescriptor(getDescriptionFromLinkTitle(links, descriptor.getRel()), descriptor); } model.add(createModelForDescriptor(descriptor)); } @@ -191,8 +181,7 @@ private List> createLinksModel(Map> links return model; } - private String getDescriptionFromLinkTitle(Map> links, - String rel) { + private String getDescriptionFromLinkTitle(Map> links, String rel) { List linksForRel = links.get(rel); if (linksForRel != null) { for (Link link : linksForRel) { @@ -201,13 +190,12 @@ private String getDescriptionFromLinkTitle(Map> links, } } } - throw new SnippetException("No description was provided for the link with rel '" - + rel + "' and no title was available from the link in the payload"); + throw new SnippetException("No description was provided for the link with rel '" + rel + + "' and no title was available from the link in the payload"); } private LinkDescriptor createDescriptor(String description, LinkDescriptor source) { - LinkDescriptor newDescriptor = new LinkDescriptor(source.getRel()) - .description(description); + LinkDescriptor newDescriptor = new LinkDescriptor(source.getRel()).description(description); if (source.isOptional()) { newDescriptor.optional(); } @@ -220,7 +208,6 @@ private LinkDescriptor createDescriptor(String description, LinkDescriptor sourc /** * Returns a {@code Map} of {@link LinkDescriptor LinkDescriptors} keyed by their * {@link LinkDescriptor#getRel() rels}. - * * @return the link descriptors */ protected final Map getDescriptorsByRel() { @@ -229,7 +216,6 @@ protected final Map getDescriptorsByRel() { /** * Returns a model for the given {@code descriptor}. - * * @param descriptor the descriptor * @return the model */ @@ -261,8 +247,7 @@ public final LinksSnippet and(LinkDescriptor... additionalDescriptors) { * @return the new snippet */ public final LinksSnippet and(List additionalDescriptors) { - List combinedDescriptors = new ArrayList<>( - this.descriptorsByRel.values()); + List combinedDescriptors = new ArrayList<>(this.descriptorsByRel.values()); combinedDescriptors.addAll(additionalDescriptors); return new LinksSnippet(this.linkExtractor, combinedDescriptors, getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java index f69279de6..038831d20 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.restdocs.operation; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import org.springframework.http.HttpHeaders; @@ -27,30 +28,35 @@ * * @author Andy Wilkinson */ -abstract class AbstractOperationMessage { +abstract class AbstractOperationMessage implements OperationMessage { private final byte[] content; private final HttpHeaders headers; AbstractOperationMessage(byte[] content, HttpHeaders headers) { - this.content = content == null ? new byte[0] : content; + this.content = (content != null) ? content : new byte[0]; this.headers = headers; } + @Override public byte[] getContent() { return Arrays.copyOf(this.content, this.content.length); } + @Override public HttpHeaders getHeaders() { return HttpHeaders.readOnlyHttpHeaders(this.headers); } + @Override public String getContentAsString() { if (this.content.length > 0) { Charset charset = extractCharsetFromContentTypeHeader(); - return charset != null ? new String(this.content, charset) - : new String(this.content); + if (charset == null) { + charset = StandardCharsets.UTF_8; + } + return new String(this.content, charset); } return ""; } @@ -63,7 +69,7 @@ private Charset extractCharsetFromContentTypeHeader() { if (contentType == null) { return null; } - return contentType.getCharSet(); + return contentType.getCharset(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java index d829bdf99..03ffd5e3d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java @@ -30,7 +30,6 @@ public class ConversionException extends RuntimeException { /** * Creates a new {@code ConversionException} with the given {@code cause}. - * * @param cause the cause */ public ConversionException(Throwable cause) { @@ -40,7 +39,6 @@ public ConversionException(Throwable cause) { /** * Creates a new {@code ConversionException} with the given {@code message} and * {@code cause}. - * * @param message the message * @param cause the cause */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/FormParameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/FormParameters.java new file mode 100644 index 000000000..5cabb84e3 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/FormParameters.java @@ -0,0 +1,96 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package org.springframework.restdocs.operation; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.LinkedList; +import java.util.List; +import java.util.Scanner; + +import org.springframework.util.LinkedMultiValueMap; + +/** + * A request's form parameters, derived from its form URL encoded body content. + * + * @author Andy Wilkinson + * @since 3.0.0 + */ +public final class FormParameters extends LinkedMultiValueMap { + + private FormParameters() { + + } + + /** + * Extracts the form parameters from the body of the given {@code request}. If the + * request has no body content, an empty {@code FormParameters} is returned, rather + * than {@code null}. + * @param request the request + * @return the form parameters extracted from the body content + */ + public static FormParameters from(OperationRequest request) { + return of(request.getContentAsString()); + } + + private static FormParameters of(String bodyContent) { + if (bodyContent == null || bodyContent.length() == 0) { + return new FormParameters(); + } + return parse(bodyContent); + } + + private static FormParameters parse(String bodyContent) { + FormParameters parameters = new FormParameters(); + try (Scanner scanner = new Scanner(bodyContent)) { + scanner.useDelimiter("&"); + while (scanner.hasNext()) { + processParameter(scanner.next(), parameters); + } + } + return parameters; + } + + private static void processParameter(String parameter, FormParameters parameters) { + String[] components = parameter.split("="); + if (components.length > 0 && components.length < 3) { + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + List values = parameters.computeIfAbsent(components[0], (p) -> new LinkedList<>()); + values.add(""); + } + } + else { + throw new IllegalArgumentException("The parameter '" + parameter + "' is malformed"); + } + } + + private static String decode(String encoded) { + try { + return URLDecoder.decode(encoded, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("Unable to URL decode " + encoded + " using UTF-8", ex); + } + + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Operation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Operation.java index 3c5792bcb..a4d956ebe 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Operation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Operation.java @@ -27,28 +27,24 @@ public interface Operation { /** * Returns a {@code Map} of attributes associated with the operation. - * * @return the attributes */ Map getAttributes(); /** * Returns the name of the operation. - * * @return the name */ String getName(); /** * Returns the request that was sent. - * * @return the request */ OperationRequest getRequest(); /** * Returns the response that was received. - * * @return the response */ OperationResponse getResponse(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationMessage.java similarity index 62% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationMessage.java index 366f8174f..7570c5ed1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,17 +14,21 @@ * limitations under the License. */ -package org.springframework.restdocs.cli; +package org.springframework.restdocs.operation; + +import org.springframework.http.HttpHeaders; /** - * A parser for the query string of a URI. + * Base contract for operation requests, request parts, and responses. * * @author Andy Wilkinson - * @deprecated since 1.1.2 in favor of - * {@link org.springframework.restdocs.operation.QueryStringParser} */ -@Deprecated -public class QueryStringParser - extends org.springframework.restdocs.operation.QueryStringParser { +interface OperationMessage { + + byte[] getContent(); + + String getContentAsString(); + + HttpHeaders getHeaders(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java index 0c3b02cc2..7e36c2f37 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,6 @@ public interface OperationRequest { /** * Returns the content of the request. If the request has no content an empty array is * returned. - * * @return the contents, never {@code null} */ byte[] getContent(); @@ -43,47 +42,41 @@ public interface OperationRequest { * content an empty string is returned. If the request has a {@code Content-Type} * header that specifies a charset then that charset will be used when converting the * contents to a {@code String}. - * * @return the contents as string, never {@code null} */ String getContentAsString(); /** * Returns the headers that were included in the request. - * * @return the headers */ HttpHeaders getHeaders(); /** * Returns the HTTP method of the request. - * * @return the HTTP method */ HttpMethod getMethod(); - /** - * Returns the request's parameters. For a {@code GET} request, the parameters are - * derived from the query string. For a {@code POST} request, the parameters are - * derived form the request's body. - * - * @return the parameters - */ - Parameters getParameters(); - /** * Returns the request's parts, provided that it is a multipart request. If not, then * an empty {@link Collection} is returned. - * * @return the parts */ Collection getParts(); /** * Returns the request's URI. - * * @return the URI */ URI getUri(); + /** + * Returns the {@link RequestCookie cookies} sent with the request. If no cookies were + * sent an empty collection is returned. + * @return the cookies, never {@code null} + * @since 1.2.0 + */ + Collection getCookies(); + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java index 73e01a59a..39d0f5f73 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.net.URI; import java.util.Collection; +import java.util.Collections; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -33,75 +34,67 @@ public class OperationRequestFactory { * Creates a new {@link OperationRequest}. The given {@code headers} will be augmented * to ensure that they always include a {@code Content-Length} header if the request * has any content and a {@code Host} header. - * * @param uri the request's uri * @param method the request method * @param content the content of the request * @param headers the request's headers - * @param parameters the request's parameters * @param parts the request's parts + * @param cookies the request's cookies * @return the {@code OperationRequest} + * @since 3.0.0 */ - public OperationRequest create(URI uri, HttpMethod method, byte[] content, - HttpHeaders headers, Parameters parameters, + public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, + Collection parts, Collection cookies) { + return new StandardOperationRequest(uri, method, content, augmentHeaders(headers, uri, content), + (parts != null) ? parts : Collections.emptyList(), cookies); + } + + /** + * Creates a new {@link OperationRequest}. The given {@code headers} will be augmented + * to ensure that they always include a {@code Content-Length} header if the request + * has any content and a {@code Host} header. + * @param uri the request's uri + * @param method the request method + * @param content the content of the request + * @param headers the request's headers + * @param parts the request's parts + * @return the {@code OperationRequest} + * @since 3.0.0 + */ + public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Collection parts) { - return new StandardOperationRequest(uri, method, content, - augmentHeaders(headers, uri, content), parameters, parts); + return create(uri, method, content, headers, parts, Collections.emptyList()); } /** * Creates a new {@code OperationRequest} based on the given {@code original} but with * the given {@code newContent}. If the original request had a {@code Content-Length} * header it will be modified to match the length of the new content. - * - * @param original The original request - * @param newContent The new content - * - * @return The new request with the new content + * @param original the original request + * @param newContent the new content + * @return the new request with the new content */ public OperationRequest createFrom(OperationRequest original, byte[] newContent) { - return new StandardOperationRequest(original.getUri(), original.getMethod(), - newContent, getUpdatedHeaders(original.getHeaders(), newContent), - original.getParameters(), original.getParts()); + return new StandardOperationRequest(original.getUri(), original.getMethod(), newContent, + getUpdatedHeaders(original.getHeaders(), newContent), original.getParts(), original.getCookies()); } /** * Creates a new {@code OperationRequest} based on the given {@code original} but with * the given {@code newHeaders}. - * - * @param original The original request - * @param newHeaders The new headers - * - * @return The new request with the new headers - */ - public OperationRequest createFrom(OperationRequest original, - HttpHeaders newHeaders) { - return new StandardOperationRequest(original.getUri(), original.getMethod(), - original.getContent(), newHeaders, original.getParameters(), - original.getParts()); - } - - /** - * Creates a new {@code OperationRequest} based on the given {@code original} but with - * the given {@code newParameters}. - * - * @param original The original request - * @param newParameters The new parameters - * - * @return The new request with the new parameters + * @param original the original request + * @param newHeaders the new headers + * @return the new request with the new headers */ - public OperationRequest createFrom(OperationRequest original, - Parameters newParameters) { - return new StandardOperationRequest(original.getUri(), original.getMethod(), - original.getContent(), original.getHeaders(), newParameters, - original.getParts()); + public OperationRequest createFrom(OperationRequest original, HttpHeaders newHeaders) { + return new StandardOperationRequest(original.getUri(), original.getMethod(), original.getContent(), newHeaders, + original.getParts(), original.getCookies()); } - private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, - byte[] content) { - return new HttpHeadersHelper(originalHeaders) - .addIfAbsent(HttpHeaders.HOST, createHostHeader(uri)) - .setContentLengthHeader(content).getHeaders(); + private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, byte[] content) { + return new HttpHeadersHelper(originalHeaders).addIfAbsent(HttpHeaders.HOST, createHostHeader(uri)) + .setContentLengthHeader(content) + .getHeaders(); } private String createHostHeader(URI uri) { @@ -111,10 +104,8 @@ private String createHostHeader(URI uri) { return uri.getHost() + ":" + uri.getPort(); } - private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, - byte[] updatedContent) { - return new HttpHeadersHelper(originalHeaders) - .updateContentLengthHeaderIfPresent(updatedContent).getHeaders(); + private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, byte[] updatedContent) { + return new HttpHeadersHelper(originalHeaders).updateContentLengthHeaderIfPresent(updatedContent).getHeaders(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java index f99057167..77dd7f2dc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java @@ -28,21 +28,18 @@ public interface OperationRequestPart { /** * Returns the name of the part. - * * @return the name */ String getName(); /** * Returns the name of the file that is being uploaded in this part. - * * @return the name of the file */ String getSubmittedFileName(); /** * Returns the contents of the part. - * * @return the contents */ byte[] getContent(); @@ -52,14 +49,12 @@ public interface OperationRequestPart { * empty string is returned. If the part has a {@code Content-Type} header that * specifies a charset then that charset will be used when converting the contents to * a {@code String}. - * * @return the contents as string, never {@code null} */ String getContentAsString(); /** * Returns the part's headers. - * * @return the headers */ HttpHeaders getHeaders(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPartFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPartFactory.java index f72a721a6..d9fe354c6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPartFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPartFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,17 +29,14 @@ public class OperationRequestPartFactory { * Creates a new {@link OperationRequestPart}. The given {@code headers} will be * augmented to ensure that they always include a {@code Content-Length} header if the * part has any content. - * * @param name the name of the part * @param submittedFileName the name of the file being submitted by the part * @param content the content of the part * @param headers the headers of the part * @return the {@code OperationRequestPart} */ - public OperationRequestPart create(String name, String submittedFileName, - byte[] content, HttpHeaders headers) { - return new StandardOperationRequestPart(name, submittedFileName, content, - augmentHeaders(headers, content)); + public OperationRequestPart create(String name, String submittedFileName, byte[] content, HttpHeaders headers) { + return new StandardOperationRequestPart(name, submittedFileName, content, augmentHeaders(headers, content)); } private HttpHeaders augmentHeaders(HttpHeaders input, byte[] content) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java index 212225083..344b4e9a9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,16 @@ package org.springframework.restdocs.operation; +import java.util.Collection; + import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; /** * The response that was received as part of performing an operation on a RESTful service. * * @author Andy Wilkinson + * @author Clyde Stubbs * @see Operation * @see Operation#getRequest() */ @@ -30,14 +33,12 @@ public interface OperationResponse { /** * Returns the status of the response. - * - * @return the status + * @return the status, never {@code null} */ - HttpStatus getStatus(); + HttpStatusCode getStatus(); /** * Returns the headers in the response. - * * @return the headers */ HttpHeaders getHeaders(); @@ -45,7 +46,6 @@ public interface OperationResponse { /** * Returns the content of the response. If the response has no content an empty array * is returned. - * * @return the contents, never {@code null} */ byte[] getContent(); @@ -55,9 +55,16 @@ public interface OperationResponse { * content an empty string is returned. If the response has a {@code Content-Type} * header that specifies a charset then that charset will be used when converting the * contents to a {@code String}. - * * @return the contents as string, never {@code null} */ String getContentAsString(); + /** + * Returns the {@link ResponseCookie cookies} returned with the response. If no + * cookies were returned an empty collection is returned. + * @return the cookies, never {@code null} + * @since 3.0 + */ + Collection getCookies(); + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java index 3a1f8b86c..b9c749ab1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,30 +16,49 @@ package org.springframework.restdocs.operation; +import java.util.Collection; +import java.util.Collections; + import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; /** * A factory for creating {@link OperationResponse OperationResponses}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ public class OperationResponseFactory { + /** + * Creates a new {@link OperationResponse} without cookies. If the response has any + * content, the given {@code headers} will be augmented to ensure that they include a + * {@code Content-Length} header. + * @param status the status of the response + * @param headers the request's headers + * @param content the content of the request + * @return the {@code OperationResponse} + * @since 3.0.0 + */ + public OperationResponse create(HttpStatusCode status, HttpHeaders headers, byte[] content) { + return new StandardOperationResponse(status, augmentHeaders(headers, content), content, + Collections.emptyList()); + } + /** * Creates a new {@link OperationResponse}. If the response has any content, the given * {@code headers} will be augmented to ensure that they include a * {@code Content-Length} header. - * * @param status the status of the response * @param headers the request's headers * @param content the content of the request + * @param cookies the cookies * @return the {@code OperationResponse} + * @since 3.0.0 */ - public OperationResponse create(HttpStatus status, HttpHeaders headers, - byte[] content) { - return new StandardOperationResponse(status, augmentHeaders(headers, content), - content); + public OperationResponse create(HttpStatusCode status, HttpHeaders headers, byte[] content, + Collection cookies) { + return new StandardOperationResponse(status, augmentHeaders(headers, content), content, cookies); } /** @@ -47,41 +66,33 @@ public OperationResponse create(HttpStatus status, HttpHeaders headers, * with the given {@code newContent}. If the original response had a * {@code Content-Length} header it will be modified to match the length of the new * content. - * - * @param original The original response - * @param newContent The new content - * - * @return The new response with the new content + * @param original the original response + * @param newContent the new content + * @return the new response with the new content */ public OperationResponse createFrom(OperationResponse original, byte[] newContent) { - return new StandardOperationResponse(original.getStatus(), - getUpdatedHeaders(original.getHeaders(), newContent), newContent); + return new StandardOperationResponse(original.getStatus(), getUpdatedHeaders(original.getHeaders(), newContent), + newContent, original.getCookies()); } /** * Creates a new {@code OperationResponse} based on the given {@code original} but * with the given {@code newHeaders}. - * - * @param original The original response - * @param newHeaders The new headers - * - * @return The new response with the new headers + * @param original the original response + * @param newHeaders the new headers + * @return the new response with the new headers */ - public OperationResponse createFrom(OperationResponse original, - HttpHeaders newHeaders) { - return new StandardOperationResponse(original.getStatus(), newHeaders, - original.getContent()); + public OperationResponse createFrom(OperationResponse original, HttpHeaders newHeaders) { + return new StandardOperationResponse(original.getStatus(), newHeaders, original.getContent(), + original.getCookies()); } private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, byte[] content) { - return new HttpHeadersHelper(originalHeaders).setContentLengthHeader(content) - .getHeaders(); + return new HttpHeadersHelper(originalHeaders).setContentLengthHeader(content).getHeaders(); } - private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, - byte[] updatedContent) { - return new HttpHeadersHelper(originalHeaders) - .updateContentLengthHeaderIfPresent(updatedContent).getHeaders(); + private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, byte[] updatedContent) { + return new HttpHeadersHelper(originalHeaders).updateContentLengthHeaderIfPresent(updatedContent).getHeaders(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java deleted file mode 100644 index d8a30b71f..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.operation; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URLEncoder; -import java.util.List; -import java.util.Map; - -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.StringUtils; - -/** - * The parameters received in a request. - * - * @author Andy Wilkinson - */ -@SuppressWarnings("serial") -public class Parameters extends LinkedMultiValueMap { - - /** - * Converts the parameters to a query string suitable for use in a URI or the body of - * a form-encoded request. - * - * @return the query string - */ - public String toQueryString() { - StringBuilder sb = new StringBuilder(); - for (Map.Entry> entry : entrySet()) { - if (entry.getValue().isEmpty()) { - append(sb, entry.getKey()); - } - else { - for (String value : entry.getValue()) { - append(sb, entry.getKey(), value); - } - } - } - return sb.toString(); - } - - /** - * Returns a new {@code Parameters} containing only the parameters that do no appear - * in the query string of the given {@code uri}. - * - * @param uri the uri - * @return the unique parameters - */ - public Parameters getUniqueParameters(URI uri) { - Parameters queryStringParameters = new QueryStringParser().parse(uri); - Parameters uniqueParameters = new Parameters(); - - for (Map.Entry> parameter : entrySet()) { - addIfUnique(parameter, queryStringParameters, uniqueParameters); - } - return uniqueParameters; - } - - private void addIfUnique(Map.Entry> parameter, - Parameters queryStringParameters, Parameters uniqueParameters) { - if (!queryStringParameters.containsKey(parameter.getKey())) { - uniqueParameters.put(parameter.getKey(), parameter.getValue()); - } - else { - List candidates = parameter.getValue(); - List existing = queryStringParameters.get(parameter.getKey()); - for (String candidate : candidates) { - if (!existing.contains(candidate)) { - uniqueParameters.add(parameter.getKey(), candidate); - } - } - } - } - - private static void append(StringBuilder sb, String key) { - append(sb, key, ""); - } - - private static void append(StringBuilder sb, String key, String value) { - doAppend(sb, urlEncodeUTF8(key) + "=" + urlEncodeUTF8(value)); - } - - private static void doAppend(StringBuilder sb, String toAppend) { - if (sb.length() > 0) { - sb.append("&"); - } - sb.append(toAppend); - } - - private static String urlEncodeUTF8(String s) { - if (!StringUtils.hasLength(s)) { - return ""; - } - try { - return URLEncoder.encode(s, "UTF-8"); - } - catch (UnsupportedEncodingException ex) { - throw new IllegalStateException("Unable to URL encode " + s + " using UTF-8", - ex); - } - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryParameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryParameters.java new file mode 100644 index 000000000..17f7fc1c6 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryParameters.java @@ -0,0 +1,90 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package org.springframework.restdocs.operation; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.LinkedList; +import java.util.List; +import java.util.Scanner; + +import org.springframework.util.LinkedMultiValueMap; + +/** + * A request's query parameters, derived from its URI's query string. + * + * @author Andy Wilkinson + * @since 3.0.0 + */ +public final class QueryParameters extends LinkedMultiValueMap { + + private QueryParameters() { + + } + + /** + * Extracts the query parameters from the query string of the given {@code request}. + * If the request has no query string, an empty {@code QueryParameters} is returned, + * rather than {@code null}. + * @param request the request + * @return the query parameters extracted from the request's query string + */ + public static QueryParameters from(OperationRequest request) { + return from(request.getUri().getRawQuery()); + } + + private static QueryParameters from(String queryString) { + if (queryString == null || queryString.length() == 0) { + return new QueryParameters(); + } + return parse(queryString); + } + + private static QueryParameters parse(String query) { + QueryParameters parameters = new QueryParameters(); + try (Scanner scanner = new Scanner(query)) { + scanner.useDelimiter("&"); + while (scanner.hasNext()) { + processParameter(scanner.next(), parameters); + } + } + return parameters; + } + + private static void processParameter(String parameter, QueryParameters parameters) { + String[] components = parameter.split("="); + if (components.length > 0 && components.length < 3) { + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + List values = parameters.computeIfAbsent(components[0], (p) -> new LinkedList<>()); + values.add(""); + } + } + else { + throw new IllegalArgumentException("The parameter '" + parameter + "' is malformed"); + } + } + + private static String decode(String encoded) { + return URLDecoder.decode(encoded, StandardCharsets.UTF_8); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java deleted file mode 100644 index 75c2bc15d..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.operation; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URLDecoder; -import java.util.LinkedList; -import java.util.List; -import java.util.Scanner; - -/** - * A parser for the query string of a URI. - * - * @author Andy Wilkinson - */ -public class QueryStringParser { - - /** - * Parses the query string of the given {@code uri} and returns the resulting - * {@link Parameters}. - * - * @param uri the uri to parse - * @return the parameters parsed from the query string - */ - public Parameters parse(URI uri) { - String query = uri.getRawQuery(); - if (query != null) { - return parse(query); - } - return new Parameters(); - } - - private Parameters parse(String query) { - Parameters parameters = new Parameters(); - try (Scanner scanner = new Scanner(query)) { - scanner.useDelimiter("&"); - while (scanner.hasNext()) { - processParameter(scanner.next(), parameters); - } - } - return parameters; - } - - private void processParameter(String parameter, Parameters parameters) { - String[] components = parameter.split("="); - if (components.length > 0 && components.length < 3) { - if (components.length == 2) { - String name = components[0]; - String value = components[1]; - parameters.add(decode(name), decode(value)); - } - else { - List values = parameters.get(components[0]); - if (values == null) { - parameters.put(components[0], new LinkedList()); - } - } - } - else { - throw new IllegalArgumentException( - "The parameter '" + parameter + "' is malformed"); - } - } - - private String decode(String encoded) { - try { - return URLDecoder.decode(encoded, "UTF-8"); - } - catch (UnsupportedEncodingException ex) { - throw new IllegalStateException( - "Unable to URL encode " + encoded + " using UTF-8", ex); - } - - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java index fac2628f9..896893107 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ * A {@code RequestConverter} is used to convert an implementation-specific request into * an {@link OperationRequest}. * - * @param The implementation-specific request type + * @param the implementation-specific request type * @author Andy Wilkinson * @since 1.1.0 */ @@ -28,7 +28,6 @@ public interface RequestConverter { /** * Converts the given {@code request} into an {@code OperationRequest}. - * * @param request the request * @return the operation request * @throws ConversionException if the conversion fails diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestCookie.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestCookie.java new file mode 100644 index 000000000..4211ef12c --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestCookie.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2018 the original author or 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. + */ + +package org.springframework.restdocs.operation; + +/** + * A representation of a Cookie received in a request. + * + * @author Andy Wilkinson + * @since 1.2.0 + */ +public final class RequestCookie { + + private final String name; + + private final String value; + + /** + * Creates a new {@code RequestCookie} with the given {@code name} and {@code value}. + * @param name the name of the cookie + * @param value the value of the cookie + */ + public RequestCookie(String name, String value) { + this.name = name; + this.value = value; + } + + /** + * Returns the name of the cookie. + * @return the name + */ + public String getName() { + return this.name; + } + + /** + * Returns the value of the cookie. + * @return the value + */ + public String getValue() { + return this.value; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java index 42a7b59df..6955b9c37 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ * A {@code ResponseConverter} is used to convert an implementation-specific response into * an {@link OperationResponse}. * - * @param The implementation-specific response type + * @param the implementation-specific response type * @author Andy Wilkinson * @since 1.1.0 */ @@ -28,7 +28,6 @@ public interface ResponseConverter { /** * Converts the given {@code response} into an {@code OperationResponse}. - * * @param response the response * @return the operation response * @throws ConversionException if the conversion fails diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java new file mode 100644 index 000000000..9cf39302e --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package org.springframework.restdocs.operation; + +/** + * A representation of a Cookie returned in a response. + * + * @author Clyde Stubbs + * @since 3.0 + */ +public final class ResponseCookie { + + private final String name; + + private final String value; + + /** + * Creates a new {@code ResponseCookie} with the given {@code name} and {@code value}. + * @param name the name of the cookie + * @param value the value of the cookie + */ + public ResponseCookie(String name, String value) { + this.name = name; + this.value = value; + } + + /** + * Returns the name of the cookie. + * @return the name + */ + public String getName() { + return this.name; + } + + /** + * Returns the value of the cookie. + * @return the value + */ + public String getValue() { + return this.value; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperation.java index f39751ad2..14a62de2e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,14 +35,13 @@ public class StandardOperation implements Operation { /** * Creates a new {@code StandardOperation}. - * * @param name the name of the operation * @param request the request that was sent * @param response the response that was received * @param attributes attributes to associate with the operation */ - public StandardOperation(String name, OperationRequest request, - OperationResponse response, Map attributes) { + public StandardOperation(String name, OperationRequest request, OperationResponse response, + Map attributes) { this.name = name; this.request = request; this.response = response; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index c12c58544..bdf65934a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,36 +28,34 @@ * * @author Andy Wilkinson */ -class StandardOperationRequest extends AbstractOperationMessage - implements OperationRequest { +class StandardOperationRequest extends AbstractOperationMessage implements OperationRequest { private HttpMethod method; - private Parameters parameters; - private Collection parts; private URI uri; + private Collection cookies; + /** * Creates a new request with the given {@code uri} and {@code method}. The request - * will have the given {@code headers}, {@code parameters}, and {@code parts}. - * + * will have the given {@code headers}, {@code parameters}, {@code parts}, and + * {@code cookies}. * @param uri the uri * @param method the method * @param content the content * @param headers the headers - * @param parameters the parameters * @param parts the parts + * @param cookies the cookies */ - StandardOperationRequest(URI uri, HttpMethod method, byte[] content, - HttpHeaders headers, Parameters parameters, - Collection parts) { + StandardOperationRequest(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, + Collection parts, Collection cookies) { super(content, headers); this.uri = uri; this.method = method; - this.parameters = parameters; this.parts = parts; + this.cookies = cookies; } @Override @@ -65,11 +63,6 @@ public HttpMethod getMethod() { return this.method; } - @Override - public Parameters getParameters() { - return this.parameters; - } - @Override public Collection getParts() { return Collections.unmodifiableCollection(this.parts); @@ -80,4 +73,9 @@ public URI getUri() { return this.uri; } + @Override + public Collection getCookies() { + return this.cookies; + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java index 825e1c962..2a9e9b50e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,7 @@ * * @author Andy Wilkinson */ -class StandardOperationRequestPart extends AbstractOperationMessage - implements OperationRequestPart { +class StandardOperationRequestPart extends AbstractOperationMessage implements OperationRequestPart { private final String name; @@ -32,14 +31,12 @@ class StandardOperationRequestPart extends AbstractOperationMessage /** * Creates a new {@code StandardOperationRequestPart} with the given {@code name}. - * * @param name the name of the part * @param submittedFileName the name of the file being uploaded by this part * @param content the contents of the part * @param headers the headers of the part */ - StandardOperationRequestPart(String name, String submittedFileName, byte[] content, - HttpHeaders headers) { + StandardOperationRequestPart(String name, String submittedFileName, byte[] content, HttpHeaders headers) { super(content, headers); this.name = name; this.submittedFileName = submittedFileName; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java index 2ab6ff88b..667d03ca8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,35 +16,46 @@ package org.springframework.restdocs.operation; +import java.util.Collection; + import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; /** * Standard implementation of {@link OperationResponse}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ -class StandardOperationResponse extends AbstractOperationMessage - implements OperationResponse { +class StandardOperationResponse extends AbstractOperationMessage implements OperationResponse { + + private final HttpStatusCode status; - private final HttpStatus status; + private Collection cookies; /** * Creates a new response with the given {@code status}, {@code headers}, and * {@code content}. - * * @param status the status of the response * @param headers the headers of the response * @param content the content of the response + * @param cookies any cookies included in the response */ - StandardOperationResponse(HttpStatus status, HttpHeaders headers, byte[] content) { + StandardOperationResponse(HttpStatusCode status, HttpHeaders headers, byte[] content, + Collection cookies) { super(content, headers); this.status = status; + this.cookies = cookies; } @Override - public HttpStatus getStatus() { + public HttpStatusCode getStatus() { return this.status; } + @Override + public Collection getCookies() { + return this.cookies; + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java index f443fe115..e840f960f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifier.java @@ -32,7 +32,6 @@ public interface ContentModifier { /** * Returns modified content based on the given {@code originalContent}. - * * @param originalContent the original content * @param contentType the type of the original content, may be {@code null} * @return the modified content diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java index 10caf9512..82bacd235 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java @@ -38,7 +38,6 @@ public class ContentModifyingOperationPreprocessor implements OperationPreproces /** * Create a new {@code ContentModifyingOperationPreprocessor} that will apply the * given {@code contentModifier} to the operation's request or response. - * * @param contentModifier the contentModifier */ public ContentModifyingOperationPreprocessor(ContentModifier contentModifier) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java index 7302f8e15..f99b4b111 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessor.java @@ -37,7 +37,6 @@ class DelegatingOperationRequestPreprocessor implements OperationRequestPreproce * Creates a new {@code DelegatingOperationRequestPreprocessor} that will delegate to * the given {@code delegates} by calling * {@link OperationPreprocessor#preprocess(OperationRequest)}. - * * @param delegates the delegates */ DelegatingOperationRequestPreprocessor(List delegates) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java index 48ab020d3..48345eaf9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessor.java @@ -36,7 +36,6 @@ class DelegatingOperationResponsePreprocessor implements OperationResponsePrepro * Creates a new {@code DelegatingOperationResponsePreprocessor} that will delegate to * the given {@code delegates} by calling * {@link OperationPreprocessor#preprocess(OperationResponse)}. - * * @param delegates the delegates */ DelegatingOperationResponsePreprocessor(List delegates) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderFilter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderFilter.java index 43b7810d8..744a2334e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderFilter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderFilter.java @@ -26,7 +26,6 @@ interface HeaderFilter { /** * Called to determine whether a header should be excluded. Return {@code true} to * exclude a header, otherwise {@code false}. - * * @param name the name of the header * @return {@code true} to exclude the header, otherwise {@code false} */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java deleted file mode 100644 index f8d24928b..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.operation.preprocess; - -import java.util.Iterator; - -import org.springframework.http.HttpHeaders; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.OperationResponseFactory; - -/** - * An {@link OperationPreprocessor} that removes headers. The headers to remove are - * provided as constructor arguments and can be either plain string or patterns to match - * against the headers found - * - * @author Andy Wilkinson - */ -class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { - - private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - - private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - - private final HeaderFilter headerFilter; - - HeaderRemovingOperationPreprocessor(HeaderFilter headerFilter) { - this.headerFilter = headerFilter; - } - - @Override - public OperationResponse preprocess(OperationResponse response) { - return this.responseFactory.createFrom(response, - removeHeaders(response.getHeaders())); - } - - @Override - public OperationRequest preprocess(OperationRequest request) { - return this.requestFactory.createFrom(request, - removeHeaders(request.getHeaders())); - } - - private HttpHeaders removeHeaders(HttpHeaders originalHeaders) { - HttpHeaders processedHeaders = new HttpHeaders(); - processedHeaders.putAll(originalHeaders); - Iterator headers = processedHeaders.keySet().iterator(); - while (headers.hasNext()) { - if (this.headerFilter.excludeHeader(headers.next())) { - headers.remove(); - } - } - return processedHeaders; - } -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessor.java new file mode 100644 index 000000000..001f7789d --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessor.java @@ -0,0 +1,218 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.operation.preprocess; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.util.Assert; + +/** + * An {@link OperationPreprocessor} that modifies a request or response by adding, + * setting, or removing headers. + * + * @author Jihoon Cha + * @author Andy Wilkinson + * @since 3.0.0 + */ +public class HeadersModifyingOperationPreprocessor implements OperationPreprocessor { + + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + + private final List modifications = new ArrayList<>(); + + @Override + public OperationRequest preprocess(OperationRequest request) { + return this.requestFactory.createFrom(request, preprocess(request.getHeaders())); + } + + @Override + public OperationResponse preprocess(OperationResponse response) { + return this.responseFactory.createFrom(response, preprocess(response.getHeaders())); + } + + private HttpHeaders preprocess(HttpHeaders headers) { + HttpHeaders modifiedHeaders = new HttpHeaders(); + modifiedHeaders.addAll(headers); + for (Modification modification : this.modifications) { + modification.applyTo(modifiedHeaders); + } + return modifiedHeaders; + } + + /** + * Adds a header with the given {@code name} and {@code value}. + * @param name the name + * @param value the value + * @return {@code this} + */ + public HeadersModifyingOperationPreprocessor add(String name, String value) { + this.modifications.add(new AddHeaderModification(name, value)); + return this; + } + + /** + * Sets the header with the given {@code name} to have the given {@code values}. + * @param name the name + * @param values the values + * @return {@code this} + */ + public HeadersModifyingOperationPreprocessor set(String name, String... values) { + Assert.notEmpty(values, "At least one value must be provided"); + this.modifications.add(new SetHeaderModification(name, Arrays.asList(values))); + return this; + } + + /** + * Removes the header with the given {@code name}. + * @param name the name of the parameter + * @return {@code this} + */ + public HeadersModifyingOperationPreprocessor remove(String name) { + this.modifications.add(new RemoveHeaderModification(name)); + return this; + } + + /** + * Removes the given {@code value} from the header with the given {@code name}. + * @param name the name + * @param value the value + * @return {@code this} + */ + public HeadersModifyingOperationPreprocessor remove(String name, String value) { + this.modifications.add(new RemoveValueHeaderModification(name, value)); + return this; + } + + /** + * Remove headers that match the given {@code namePattern} regular expression. + * @param namePattern the name pattern + * @return {@code this} + * @see Matcher#matches() + */ + public HeadersModifyingOperationPreprocessor removeMatching(String namePattern) { + this.modifications.add(new RemoveHeadersByNamePatternModification(Pattern.compile(namePattern))); + return this; + } + + private interface Modification { + + void applyTo(HttpHeaders headers); + + } + + private static final class AddHeaderModification implements Modification { + + private final String name; + + private final String value; + + private AddHeaderModification(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public void applyTo(HttpHeaders headers) { + headers.add(this.name, this.value); + } + + } + + private static final class SetHeaderModification implements Modification { + + private final String name; + + private final List values; + + private SetHeaderModification(String name, List values) { + this.name = name; + this.values = values; + } + + @Override + public void applyTo(HttpHeaders headers) { + headers.put(this.name, this.values); + } + + } + + private static final class RemoveHeaderModification implements Modification { + + private final String name; + + private RemoveHeaderModification(String name) { + this.name = name; + } + + @Override + public void applyTo(HttpHeaders headers) { + headers.remove(this.name); + } + + } + + private static final class RemoveValueHeaderModification implements Modification { + + private final String name; + + private final String value; + + private RemoveValueHeaderModification(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public void applyTo(HttpHeaders headers) { + List values = headers.get(this.name); + if (values != null) { + values.remove(this.value); + if (values.isEmpty()) { + headers.remove(this.name); + } + } + } + + } + + private static final class RemoveHeadersByNamePatternModification implements Modification { + + private final Pattern namePattern; + + private RemoveHeadersByNamePatternModification(Pattern namePattern) { + this.namePattern = namePattern; + } + + @Override + public void applyTo(HttpHeaders headers) { + headers.headerNames().removeIf((name) -> this.namePattern.matcher(name).matches()); + } + + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java index 9d9b95c88..09b12ff95 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.restdocs.operation.preprocess; +import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; import org.springframework.http.MediaType; @@ -29,8 +30,7 @@ class LinkMaskingContentModifier implements ContentModifier { private static final String DEFAULT_MASK = "..."; - private static final Pattern LINK_HREF = Pattern.compile("\"href\"\\s*:\\s*\"(.*?)\"", - Pattern.DOTALL); + private static final Pattern LINK_HREF = Pattern.compile("\"href\"\\s*:\\s*\"(.*?)\"", Pattern.DOTALL); private final ContentModifier contentModifier; @@ -39,7 +39,7 @@ class LinkMaskingContentModifier implements ContentModifier { } LinkMaskingContentModifier(String mask) { - this.contentModifier = new PatternReplacingContentModifier(LINK_HREF, mask); + this.contentModifier = new PatternReplacingContentModifier(LINK_HREF, mask, StandardCharsets.UTF_8); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java index 68fc6d0db..6b8fd1446 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessor.java @@ -30,7 +30,6 @@ public interface OperationPreprocessor { /** * Processes the given {@code request}. - * * @param request the request to process * @return the processed request */ @@ -38,7 +37,6 @@ public interface OperationPreprocessor { /** * Processes the given {@code response}. - * * @param response the response to process * @return the processed response */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java index e35d51b00..ddfd7aa30 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java @@ -31,7 +31,6 @@ public abstract class OperationPreprocessorAdapter implements OperationPreproces /** * Returns the given {@code request} as-is. - * * @param request the request * @return the unmodified request */ @@ -42,7 +41,6 @@ public OperationRequest preprocess(OperationRequest request) { /** * Returns the given {@code response} as-is. - * * @param response the response * @return the unmodified response */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java index 5620dd839..cfc38bd77 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationRequestPreprocessor.java @@ -29,7 +29,6 @@ public interface OperationRequestPreprocessor { /** * Processes and potentially modifies the given {@code request} before it is * documented. - * * @param request the request * @return the modified request */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java index 9db284d47..97f3400c6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationResponsePreprocessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import org.springframework.restdocs.operation.OperationResponse; /** - * An {@code OperationRequestPreprocessor} is used to modify an {@code OperationRequest} + * An {@code OperationResponsePreprocessor} is used to modify an {@code OperationResponse} * prior to it being documented. * * @author Andy Wilkinson @@ -29,7 +29,6 @@ public interface OperationResponsePreprocessor { /** * Processes and potentially modifies the given {@code response} before it is * documented. - * * @param response the response * @return the modified response */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java deleted file mode 100644 index 8d87e0388..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.operation.preprocess; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.Parameters; -import org.springframework.util.Assert; - -/** - * An {@link OperationPreprocessor} that can be used to modify a request's - * {@link OperationRequest#getParameters()} by adding, setting, and removing parameters. - * - * @author Andy Wilkinson - * @since 1.1.0 - */ -public final class ParametersModifyingOperationPreprocessor - extends OperationPreprocessorAdapter { - - private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - - private final List modifications = new ArrayList<>(); - - @Override - public OperationRequest preprocess(OperationRequest request) { - Parameters parameters = new Parameters(); - parameters.putAll(request.getParameters()); - for (Modification modification : this.modifications) { - modification.apply(parameters); - } - return this.requestFactory.createFrom(request, parameters); - } - - /** - * Adds a parameter with the given {@code name} and {@code value}. - * - * @param name the name - * @param value the value - * @return {@code this} - */ - public ParametersModifyingOperationPreprocessor add(String name, String value) { - this.modifications.add(new AddParameterModification(name, value)); - return this; - } - - /** - * Sets the parameter with the given {@code name} to have the given {@code values}. - * - * @param name the name - * @param values the values - * @return {@code this} - */ - public ParametersModifyingOperationPreprocessor set(String name, String... values) { - Assert.notEmpty(values, "At least one value must be provided"); - this.modifications.add(new SetParameterModification(name, Arrays.asList(values))); - return this; - } - - /** - * Removes the parameter with the given {@code name}. - * - * @param name the name of the parameter - * @return {@code this} - */ - public ParametersModifyingOperationPreprocessor remove(String name) { - this.modifications.add(new RemoveParameterModification(name)); - return this; - } - - /** - * Removes the given {@code value} from the parameter with the given {@code name}. - * - * @param name the name - * @param value the value - * @return {@code this} - */ - public ParametersModifyingOperationPreprocessor remove(String name, String value) { - this.modifications.add(new RemoveValueParameterModification(name, value)); - return this; - } - - private interface Modification { - - void apply(Parameters parameters); - - } - - private static final class AddParameterModification implements Modification { - - private final String name; - - private final String value; - - private AddParameterModification(String name, String value) { - this.name = name; - this.value = value; - } - - @Override - public void apply(Parameters parameters) { - parameters.add(this.name, this.value); - } - - } - - private static final class SetParameterModification implements Modification { - - private final String name; - - private final List values; - - private SetParameterModification(String name, List values) { - this.name = name; - this.values = values; - } - - @Override - public void apply(Parameters parameters) { - parameters.put(this.name, this.values); - } - - } - - private static final class RemoveParameterModification implements Modification { - - private final String name; - - private RemoveParameterModification(String name) { - this.name = name; - } - - @Override - public void apply(Parameters parameters) { - parameters.remove(this.name); - } - - } - - private static final class RemoveValueParameterModification implements Modification { - - private final String name; - - private final String value; - - private RemoveValueParameterModification(String name, String value) { - this.name = name; - this.value = value; - } - - @Override - public void apply(Parameters parameters) { - List values = parameters.get(this.name); - if (values != null) { - values.remove(this.value); - if (values.isEmpty()) { - parameters.remove(this.name); - } - } - } - - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java index 6d669d6db..1ac22beff 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.operation.preprocess; +import java.nio.charset.Charset; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -34,27 +35,40 @@ class PatternReplacingContentModifier implements ContentModifier { private final String replacement; + private final Charset fallbackCharset; + /** * Creates a new {@link PatternReplacingContentModifier} that will replace occurrences - * of the given {@code pattern} with the given {@code replacement}. - * + * of the given {@code pattern} with the given {@code replacement}. The content is + * handled using the charset from its content type. When no content type is specified + * the JVM's {@link Charset#defaultCharset() default charset is used}. * @param pattern the pattern * @param replacement the replacement */ PatternReplacingContentModifier(Pattern pattern, String replacement) { + this(pattern, replacement, Charset.defaultCharset()); + } + + /** + * Creates a new {@link PatternReplacingContentModifier} that will replace occurrences + * of the given {@code pattern} with the given {@code replacement}. The content is + * handled using the charset from its content type. When no content type is specified + * the given {@code fallbackCharset} is used. + * @param pattern the pattern + * @param replacement the replacement + * @param fallbackCharset the charset to use as a fallback + */ + PatternReplacingContentModifier(Pattern pattern, String replacement, Charset fallbackCharset) { this.pattern = pattern; this.replacement = replacement; + this.fallbackCharset = fallbackCharset; } @Override public byte[] modifyContent(byte[] content, MediaType contentType) { - String original; - if (contentType != null && contentType.getCharSet() != null) { - original = new String(content, contentType.getCharSet()); - } - else { - original = new String(content); - } + Charset charset = (contentType != null && contentType.getCharset() != null) ? contentType.getCharset() + : this.fallbackCharset; + String original = new String(content, charset); Matcher matcher = this.pattern.matcher(original); StringBuilder builder = new StringBuilder(); int previous = 0; @@ -74,7 +88,7 @@ public byte[] modifyContent(byte[] content, MediaType contentType) { if (previous < original.length()) { builder.append(original.substring(previous)); } - return builder.toString().getBytes(); + return builder.toString().getBytes(charset); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java index 1edbf77c2..6a36ed81e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ * * @author Andy Wilkinson * @author Roland Huss + * @author Jihoon Cha */ public final class Preprocessors { @@ -41,114 +42,81 @@ private Preprocessors() { /** * Returns an {@link OperationRequestPreprocessor} that will preprocess the request by * applying the given {@code preprocessors} to it. - * * @param preprocessors the preprocessors * @return the request preprocessor */ - public static OperationRequestPreprocessor preprocessRequest( - OperationPreprocessor... preprocessors) { + public static OperationRequestPreprocessor preprocessRequest(OperationPreprocessor... preprocessors) { return new DelegatingOperationRequestPreprocessor(Arrays.asList(preprocessors)); } /** * Returns an {@link OperationResponsePreprocessor} that will preprocess the response * by applying the given {@code preprocessors} to it. - * * @param preprocessors the preprocessors * @return the response preprocessor */ - public static OperationResponsePreprocessor preprocessResponse( - OperationPreprocessor... preprocessors) { + public static OperationResponsePreprocessor preprocessResponse(OperationPreprocessor... preprocessors) { return new DelegatingOperationResponsePreprocessor(Arrays.asList(preprocessors)); } /** * Returns an {@code OperationPreprocessor} that will pretty print the content of the * request or response. - * * @return the preprocessor */ public static OperationPreprocessor prettyPrint() { - return new ContentModifyingOperationPreprocessor( - new PrettyPrintingContentModifier()); - } - - /** - * Returns an {@code OperationPreprocessor} that will remove any header from the - * request or response with a name that is equal to one of the given - * {@code headersToRemove}. - * - * @param headerNames the header names - * @return the preprocessor - * @see String#equals(Object) - */ - public static OperationPreprocessor removeHeaders(String... headerNames) { - return new HeaderRemovingOperationPreprocessor( - new ExactMatchHeaderFilter(headerNames)); - } - - /** - * Returns an {@code OperationPreprocessor} that will remove any headers from the - * request or response with a name that matches one of the given - * {@code headerNamePatterns} regular expressions. - * - * @param headerNamePatterns the header name patterns - * @return the preprocessor - * @see java.util.regex.Matcher#matches() - */ - public static OperationPreprocessor removeMatchingHeaders( - String... headerNamePatterns) { - return new HeaderRemovingOperationPreprocessor( - new PatternMatchHeaderFilter(headerNamePatterns)); + return new ContentModifyingOperationPreprocessor(new PrettyPrintingContentModifier()); } /** * Returns an {@code OperationPreprocessor} that will mask the href of hypermedia * links in the request or response. - * * @return the preprocessor */ public static OperationPreprocessor maskLinks() { - return new ContentModifyingOperationPreprocessor( - new LinkMaskingContentModifier()); + return new ContentModifyingOperationPreprocessor(new LinkMaskingContentModifier()); } /** * Returns an {@code OperationPreprocessor} that will mask the href of hypermedia * links in the request or response. - * * @param mask the link mask * @return the preprocessor */ public static OperationPreprocessor maskLinks(String mask) { - return new ContentModifyingOperationPreprocessor( - new LinkMaskingContentModifier(mask)); + return new ContentModifyingOperationPreprocessor(new LinkMaskingContentModifier(mask)); } /** * Returns an {@code OperationPreprocessor} that will modify the content of the * request or response by replacing occurrences of the given {@code pattern} with the * given {@code replacement}. - * * @param pattern the pattern * @param replacement the replacement * @return the preprocessor */ - public static OperationPreprocessor replacePattern(Pattern pattern, - String replacement) { - return new ContentModifyingOperationPreprocessor( - new PatternReplacingContentModifier(pattern, replacement)); + public static OperationPreprocessor replacePattern(Pattern pattern, String replacement) { + return new ContentModifyingOperationPreprocessor(new PatternReplacingContentModifier(pattern, replacement)); + } + + /** + * Returns a {@code HeadersModifyingOperationPreprocessor} that can then be configured + * to modify the headers of the request or response. + * @return the preprocessor + * @since 3.0.0 + */ + public static HeadersModifyingOperationPreprocessor modifyHeaders() { + return new HeadersModifyingOperationPreprocessor(); } /** - * Returns a {@code ParametersModifyingOperationPreprocessor} that can then be - * configured to modify the parameters of the request. - * + * Returns a {@code UriModifyingOperationPreprocessor} that will modify URIs in the + * request or response by changing one or more of their host, scheme, and port. * @return the preprocessor - * @since 1.1.0 + * @since 2.0.1 */ - public static ParametersModifyingOperationPreprocessor modifyParameters() { - return new ParametersModifyingOperationPreprocessor(); + public static UriModifyingOperationPreprocessor modifyUris() { + return new UriModifyingOperationPreprocessor(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java index 67f0949d7..fe0105553 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import org.xml.sax.ErrorHandler; @@ -52,8 +53,7 @@ public class PrettyPrintingContentModifier implements ContentModifier { private static final List PRETTY_PRINTERS = Collections - .unmodifiableList( - Arrays.asList(new JsonPrettyPrinter(), new XmlPrettyPrinter())); + .unmodifiableList(Arrays.asList(new JsonPrettyPrinter(), new XmlPrettyPrinter())); @Override public byte[] modifyContent(byte[] originalContent, MediaType contentType) { @@ -82,44 +82,37 @@ private static final class XmlPrettyPrinter implements PrettyPrinter { public byte[] prettyPrint(byte[] original) throws Exception { Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", - "4"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes"); ByteArrayOutputStream transformed = new ByteArrayOutputStream(); transformer.setErrorListener(new SilentErrorListener()); - transformer.transform(createSaxSource(original), - new StreamResult(transformed)); + transformer.transform(createSaxSource(original), new StreamResult(transformed)); return transformed.toByteArray(); } - private SAXSource createSaxSource(byte[] original) - throws ParserConfigurationException, SAXException { + private SAXSource createSaxSource(byte[] original) throws ParserConfigurationException, SAXException { SAXParserFactory parserFactory = SAXParserFactory.newInstance(); SAXParser parser = parserFactory.newSAXParser(); XMLReader xmlReader = parser.getXMLReader(); xmlReader.setErrorHandler(new SilentErrorHandler()); - return new SAXSource(xmlReader, - new InputSource(new ByteArrayInputStream(original))); + return new SAXSource(xmlReader, new InputSource(new ByteArrayInputStream(original))); } private static final class SilentErrorListener implements ErrorListener { @Override - public void warning(TransformerException exception) - throws TransformerException { + public void warning(TransformerException exception) throws TransformerException { // Suppress } @Override - public void error(TransformerException exception) - throws TransformerException { + public void error(TransformerException exception) throws TransformerException { // Suppress } @Override - public void fatalError(TransformerException exception) - throws TransformerException { + public void fatalError(TransformerException exception) throws TransformerException { // Suppress } @@ -141,19 +134,21 @@ public void error(SAXParseException exception) throws SAXException { public void fatalError(SAXParseException exception) throws SAXException { // Suppress } + } + } private static final class JsonPrettyPrinter implements PrettyPrinter { - private final ObjectMapper objectMapper = new ObjectMapper() - .configure(SerializationFeature.INDENT_OUTPUT, true); + private final ObjectMapper objectMapper = new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true) + .configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true); @Override public byte[] prettyPrint(byte[] original) throws IOException { - return this.objectMapper - .writeValueAsBytes(this.objectMapper.readTree(original)); + return this.objectMapper.writeValueAsBytes(this.objectMapper.readTree(original)); } + } } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessor.java similarity index 79% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessor.java index fc85110cc..6e15b74b9 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.restassured.operation.preprocess; +package org.springframework.restdocs.operation.preprocess; import java.net.URI; import java.util.ArrayList; @@ -33,9 +33,6 @@ import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.preprocess.ContentModifier; -import org.springframework.restdocs.operation.preprocess.ContentModifyingOperationPreprocessor; -import org.springframework.restdocs.operation.preprocess.OperationPreprocessor; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; @@ -54,9 +51,9 @@ *

        * * @author Andy Wilkinson - * @since 1.1.0 + * @since 2.0.1 */ -public final class UriModifyingOperationPreprocessor implements OperationPreprocessor { +public class UriModifyingOperationPreprocessor implements OperationPreprocessor { private final UriModifyingContentModifier contentModifier = new UriModifyingContentModifier(); @@ -72,7 +69,6 @@ public final class UriModifyingOperationPreprocessor implements OperationPreproc /** * Modifies the URI to use the given {@code scheme}. {@code null}, the default, will * leave the scheme unchanged. - * * @param scheme the scheme * @return {@code this} */ @@ -85,7 +81,6 @@ public UriModifyingOperationPreprocessor scheme(String scheme) { /** * Modifies the URI to use the given {@code host}. {@code null}, the default, will * leave the host unchanged. - * * @param host the host * @return {@code this} */ @@ -97,7 +92,6 @@ public UriModifyingOperationPreprocessor host(String host) { /** * Modifies the URI to use the given {@code port}. - * * @param port the port * @return {@code this} */ @@ -107,7 +101,6 @@ public UriModifyingOperationPreprocessor port(int port) { /** * Removes the port from the URI. - * * @return {@code this} */ public UriModifyingOperationPreprocessor removePort() { @@ -139,24 +132,22 @@ public OperationRequest preprocess(OperationRequest request) { } URI modifiedUri = uriBuilder.build(true).toUri(); HttpHeaders modifiedHeaders = modify(request.getHeaders()); - modifiedHeaders.set(HttpHeaders.HOST, modifiedUri.getHost() - + (modifiedUri.getPort() == -1 ? "" : ":" + modifiedUri.getPort())); - return this.contentModifyingDelegate.preprocess( - new OperationRequestFactory().create(uriBuilder.build(true).toUri(), - request.getMethod(), request.getContent(), modifiedHeaders, - request.getParameters(), modify(request.getParts()))); + modifiedHeaders.set(HttpHeaders.HOST, + modifiedUri.getHost() + ((modifiedUri.getPort() != -1) ? ":" + modifiedUri.getPort() : "")); + return this.contentModifyingDelegate + .preprocess(new OperationRequestFactory().create(uriBuilder.build(true).toUri(), request.getMethod(), + request.getContent(), modifiedHeaders, modify(request.getParts()), request.getCookies())); } @Override public OperationResponse preprocess(OperationResponse response) { - return this.contentModifyingDelegate - .preprocess(new OperationResponseFactory().create(response.getStatus(), - modify(response.getHeaders()), response.getContent())); + return this.contentModifyingDelegate.preprocess(new OperationResponseFactory().create(response.getStatus(), + modify(response.getHeaders()), response.getContent(), response.getCookies())); } private HttpHeaders modify(HttpHeaders headers) { HttpHeaders modified = new HttpHeaders(); - for (Entry> header : headers.entrySet()) { + for (Entry> header : headers.headerSet()) { for (String value : header.getValue()) { modified.add(header.getKey(), this.contentModifier.modify(value)); } @@ -164,14 +155,12 @@ private HttpHeaders modify(HttpHeaders headers) { return modified; } - private Collection modify( - Collection parts) { + private Collection modify(Collection parts) { List modifiedParts = new ArrayList<>(); OperationRequestPartFactory factory = new OperationRequestPartFactory(); for (OperationRequestPart part : parts) { modifiedParts.add(factory.create(part.getName(), part.getSubmittedFileName(), - this.contentModifier.modifyContent(part.getContent(), - part.getHeaders().getContentType()), + this.contentModifier.modifyContent(part.getContent(), part.getHeaders().getContentType()), modify(part.getHeaders()))); } return modifiedParts; @@ -180,7 +169,7 @@ private Collection modify( private static final class UriModifyingContentModifier implements ContentModifier { private static final Pattern SCHEME_HOST_PORT_PATTERN = Pattern - .compile("(http[s]?)://([^/:#?]+)(:[0-9]+)?"); + .compile("(http[s]?)://([a-zA-Z0-9-\\.]+)(:[0-9]+)?"); private String scheme; @@ -203,8 +192,8 @@ private void setPort(String port) { @Override public byte[] modifyContent(byte[] content, MediaType contentType) { String input; - if (contentType != null && contentType.getCharSet() != null) { - input = new String(content, contentType.getCharSet()); + if (contentType != null && contentType.getCharset() != null) { + input = new String(content, contentType.getCharset()); } else { input = new String(content); @@ -224,13 +213,12 @@ private String modify(String input) { while (matcher.find()) { for (int i = 1; i <= matcher.groupCount(); i++) { if (matcher.start(i) >= 0) { - builder.append(input.substring(previous, matcher.start(i))); + builder.append(input, previous, matcher.start(i)); } if (matcher.start(i) >= 0) { previous = matcher.end(i); } - builder.append( - getReplacement(matcher.group(i), replacements.get(i - 1))); + builder.append(getReplacement(matcher.group(i), replacements.get(i - 1))); } } @@ -249,5 +237,7 @@ private String getReplacement(String original, String candidate) { } return ""; } + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java new file mode 100644 index 000000000..3ccd56881 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java @@ -0,0 +1,125 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.ModelCreationException; +import org.springframework.restdocs.snippet.TemplatedSnippet; + +/** + * Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that + * document a RESTful resource's request or response body. + * + * @author Andy Wilkinson + * @author Achim Grimm + */ +public abstract class AbstractBodySnippet extends TemplatedSnippet { + + private final PayloadSubsectionExtractor subsectionExtractor; + + /** + * Creates a new {@code AbstractBodySnippet} that will produce a snippet named + * {@code -body} using a template named {@code -body}. The snippet will + * contain the subsection of the body extracted by the given + * {@code subsectionExtractor}. The given {@code attributes} will be included in the + * model during template rendering + * @param type the type of the body + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + */ + protected AbstractBodySnippet(String type, PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + this(type, type, subsectionExtractor, attributes); + } + + /** + * Creates a new {@code AbstractBodySnippet} that will produce a snippet named + * {@code -body} using a template named {@code -body}. The snippet will + * contain the subsection of the body extracted by the given + * {@code subsectionExtractor}. The given {@code attributes} will be included in the + * model during template rendering + * @param name the name of the snippet + * @param type the type of the body + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + */ + protected AbstractBodySnippet(String name, String type, PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super(name + "-body" + ((subsectionExtractor != null) ? "-" + subsectionExtractor.getSubsectionId() : ""), + type + "-body", attributes); + this.subsectionExtractor = subsectionExtractor; + } + + @Override + protected Map createModel(Operation operation) { + try { + MediaType contentType = getContentType(operation); + String language = determineLanguage(contentType); + byte[] content = getContent(operation); + if (this.subsectionExtractor != null) { + content = this.subsectionExtractor.extractSubsection(content, contentType); + } + Charset charset = extractCharset(contentType); + String body = (charset != null) ? new String(content, charset) : new String(content); + Map model = new HashMap<>(); + model.put("language", language); + model.put("body", body); + return model; + } + catch (IOException ex) { + throw new ModelCreationException(ex); + } + } + + private String determineLanguage(MediaType contentType) { + if (contentType == null) { + return null; + } + return (contentType.getSubtypeSuffix() != null) ? contentType.getSubtypeSuffix() : contentType.getSubtype(); + } + + private Charset extractCharset(MediaType contentType) { + if (contentType == null) { + return null; + } + return contentType.getCharset(); + } + + /** + * Returns the content of the request or response extracted from the given + * {@code operation}. + * @param operation the operation + * @return the content + * @throws IOException if the content cannot be extracted + */ + protected abstract byte[] getContent(Operation operation) throws IOException; + + /** + * Returns the content type of the request or response extracted from the given + * {@code operation}. + * @param operation the operation + * @return the content type + */ + protected abstract MediaType getContentType(Operation operation); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 198b9e449..fe077017e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,8 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Attributes; +import org.springframework.restdocs.snippet.Attributes.Attribute; import org.springframework.restdocs.snippet.ModelCreationException; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.snippet.TemplatedSnippet; @@ -36,6 +38,7 @@ * * @author Andreas Evers * @author Andy Wilkinson + * @author Mathias Düsterhöft */ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { @@ -45,69 +48,127 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { private final String type; + private final PayloadSubsectionExtractor subsectionExtractor; + + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields} using a template named {@code -fields}. The fields will + * be documented using the given {@code descriptors} and the given {@code attributes} + * will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected AbstractFieldsSnippet(String type, List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { + this(type, type, descriptors, attributes, ignoreUndocumentedFields); + } + /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named - * {@code -fields}. The fields will be documented using the given - * {@code descriptors} and the given {@code attributes} will be included in the model - * during template rendering. Undocumented fields will trigger a failure. - * + * {@code -fields} using a template named {@code -fields}. The fields in + * the subsection of the payload extracted by the given {@code subsectionExtractor} + * will be documented using the given {@code descriptors} and the given + * {@code attributes} will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. * @param type the type of the fields * @param descriptors the field descriptors * @param attributes the additional attributes - * @deprecated since 1.1 in favor of - * {@link #AbstractFieldsSnippet(String, List, Map, boolean)} + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @param subsectionExtractor the subsection extractor + * @since 1.2.0 */ - @Deprecated - protected AbstractFieldsSnippet(String type, List descriptors, - Map attributes) { - this(type, descriptors, attributes, false); + protected AbstractFieldsSnippet(String type, List descriptors, Map attributes, + boolean ignoreUndocumentedFields, PayloadSubsectionExtractor subsectionExtractor) { + this(type, type, descriptors, attributes, ignoreUndocumentedFields, subsectionExtractor); } /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named - * {@code -fields}. The fields will be documented using the given - * {@code descriptors} and the given {@code attributes} will be included in the model - * during template rendering. If {@code ignoreUndocumentedFields} is {@code true}, - * undocumented fields will be ignored and will not trigger a failure. - * + * {@code -fields} using a template named {@code -fields}. The fields will + * be documented using the given {@code descriptors} and the given {@code attributes} + * will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * @param name the name of the snippet * @param type the type of the fields * @param descriptors the field descriptors * @param attributes the additional attributes * @param ignoreUndocumentedFields whether undocumented fields should be ignored */ - protected AbstractFieldsSnippet(String type, List descriptors, + protected AbstractFieldsSnippet(String name, String type, List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super(type + "-fields", attributes); + this(name, type, descriptors, attributes, ignoreUndocumentedFields, null); + } + + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields} using a template named {@code -fields}. The fields in + * the subsection of the payload identified by {@code subsectionPath} will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * @param name the name of the snippet + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @param subsectionExtractor the subsection extractor documented. {@code null} or an + * empty string can be used to indicate that the entire payload should be documented. + * @since 1.2.0 + */ + protected AbstractFieldsSnippet(String name, String type, List descriptors, + Map attributes, boolean ignoreUndocumentedFields, + PayloadSubsectionExtractor subsectionExtractor) { + super(name + "-fields" + ((subsectionExtractor != null) ? "-" + subsectionExtractor.getSubsectionId() : ""), + type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); if (!descriptor.isIgnored()) { - Assert.notNull(descriptor.getDescription(), - "The descriptor for field '" + descriptor.getPath() - + "' must either have a description or" + " be marked as " - + "ignored"); + Assert.notNull(descriptor.getDescription() != null, "The descriptor for '" + descriptor.getPath() + + "' must have a" + " description or it must be marked as ignored"); } - } this.fieldDescriptors = descriptors; this.ignoreUndocumentedFields = ignoreUndocumentedFields; this.type = type; + this.subsectionExtractor = subsectionExtractor; } @Override protected Map createModel(Operation operation) { - ContentHandler contentHandler = getContentHandler(operation); + byte[] content; + try { + content = verifyContent(getContent(operation)); + } + catch (IOException ex) { + throw new ModelCreationException(ex); + } + MediaType contentType = getContentType(operation); + if (this.subsectionExtractor != null) { + content = verifyContent( + this.subsectionExtractor.extractSubsection(content, contentType, this.fieldDescriptors)); + } + ContentHandler contentHandler = ContentHandler.forContentWithDescriptors(content, contentType, + this.fieldDescriptors); validateFieldDocumentation(contentHandler); + List descriptorsToDocument = new ArrayList<>(); for (FieldDescriptor descriptor : this.fieldDescriptors) { if (!descriptor.isIgnored()) { try { - descriptor.type(contentHandler.determineFieldType(descriptor)); + Object type = contentHandler.resolveFieldType(descriptor); + descriptorsToDocument.add(copyWithType(descriptor, type)); } catch (FieldDoesNotExistException ex) { - String message = "Cannot determine the type of the field '" - + descriptor.getPath() + "' as it is not present in the " - + "payload. Please provide a type using " + String message = "Cannot determine the type of the field '" + descriptor.getPath() + + "' as it is not present in the " + "payload. Please provide a type using " + "FieldDescriptor.type(Object type)."; throw new FieldTypeRequiredException(message); } @@ -117,7 +178,7 @@ protected Map createModel(Operation operation) { Map model = new HashMap<>(); List> fields = new ArrayList<>(); model.put("fields", fields); - for (FieldDescriptor descriptor : this.fieldDescriptors) { + for (FieldDescriptor descriptor : descriptorsToDocument) { if (!descriptor.isIgnored()) { fields.add(createModelForDescriptor(descriptor)); } @@ -125,42 +186,24 @@ protected Map createModel(Operation operation) { return model; } - private ContentHandler getContentHandler(Operation operation) { - MediaType contentType = getContentType(operation); - ContentHandler contentHandler; - - try { - byte[] content = getContent(operation); - if (content.length == 0) { - throw new SnippetException("Cannot document " + this.type - + " fields as the " + this.type + " body is empty"); - } - if (contentType != null - && MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { - contentHandler = new XmlContentHandler(content); - } - else { - contentHandler = new JsonContentHandler(content); - } - } - catch (IOException ex) { - throw new ModelCreationException(ex); + private byte[] verifyContent(byte[] content) { + if (content.length == 0) { + throw new SnippetException( + "Cannot document " + this.type + " fields as the " + this.type + " body is empty"); } - return contentHandler; + return content; } private void validateFieldDocumentation(ContentHandler payloadHandler) { - List missingFields = payloadHandler - .findMissingFields(this.fieldDescriptors); + List missingFields = payloadHandler.findMissingFields(); - String undocumentedPayload = this.ignoreUndocumentedFields ? null - : payloadHandler.getUndocumentedContent(this.fieldDescriptors); + String undocumentedPayload = this.ignoreUndocumentedFields ? null : payloadHandler.getUndocumentedContent(); if (!missingFields.isEmpty() || StringUtils.hasText(undocumentedPayload)) { String message = ""; if (StringUtils.hasText(undocumentedPayload)) { - message += String.format("The following parts of the payload were" - + " not documented:%n%s", undocumentedPayload); + message += String.format("The following parts of the payload were" + " not documented:%n%s", + undocumentedPayload); } if (!missingFields.isEmpty()) { if (message.length() > 0) { @@ -170,8 +213,7 @@ private void validateFieldDocumentation(ContentHandler payloadHandler) { for (FieldDescriptor fieldDescriptor : missingFields) { paths.add(fieldDescriptor.getPath()); } - message += "Fields with the following paths were not found in the" - + " payload: " + paths; + message += "Fields with the following paths were not found in the" + " payload: " + paths; } throw new SnippetException(message); } @@ -180,18 +222,16 @@ private void validateFieldDocumentation(ContentHandler payloadHandler) { /** * Returns the content type of the request or response extracted from the given * {@code operation}. - * - * @param operation The operation - * @return The content type + * @param operation the operation + * @return the content type */ protected abstract MediaType getContentType(Operation operation); /** * Returns the content of the request or response extracted form the given * {@code operation}. - * - * @param operation The operation - * @return The content + * @param operation the operation + * @return the content * @throws IOException if the content cannot be extracted */ protected abstract byte[] getContent(Operation operation) throws IOException; @@ -199,16 +239,31 @@ private void validateFieldDocumentation(ContentHandler payloadHandler) { /** * Returns the list of {@link FieldDescriptor FieldDescriptors} that will be used to * generate the documentation. - * * @return the field descriptors */ protected final List getFieldDescriptors() { return this.fieldDescriptors; } + /** + * Returns whether or not this snippet ignores undocumented fields. + * @return {@code true} if undocumented fields are ignored, otherwise {@code false} + */ + protected final boolean isIgnoredUndocumentedFields() { + return this.ignoreUndocumentedFields; + } + + /** + * Returns the {@link PayloadSubsectionExtractor}, if any, used by this snippet. + * @return the subsection extractor or {@code null} + * @since 1.2.4 + */ + protected final PayloadSubsectionExtractor getSubsectionExtractor() { + return this.subsectionExtractor; + } + /** * Returns a model for the given {@code descriptor}. - * * @param descriptor the descriptor * @return the model */ @@ -222,4 +277,25 @@ protected Map createModelForDescriptor(FieldDescriptor descripto return model; } + private FieldDescriptor copyWithType(FieldDescriptor source, Object type) { + FieldDescriptor result = (source instanceof SubsectionDescriptor) ? new SubsectionDescriptor(source.getPath()) + : new FieldDescriptor(source.getPath()); + result.description(source.getDescription()).type(type).attributes(asArray(source.getAttributes())); + if (source.isIgnored()) { + result.ignored(); + } + if (source.isOptional()) { + result.optional(); + } + return result; + } + + private static Attribute[] asArray(Map attributeMap) { + List attributes = new ArrayList<>(); + for (Map.Entry attribute : attributeMap.entrySet()) { + attributes.add(Attributes.key(attribute.getKey()).value(attribute.getValue())); + } + return attributes.toArray(new Attribute[attributes.size()]); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java index ea55b13b6..0111548c3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,44 +18,58 @@ import java.util.List; +import org.springframework.http.MediaType; + /** * A handler for the content of a request or response. * * @author Andy Wilkinson + * @author Mathias Düsterhöft */ -interface ContentHandler { +interface ContentHandler extends FieldTypeResolver { /** * Finds the fields that are missing from the handler's payload. A field is missing if - * it is described by one of the {@code fieldDescriptors} but is not present in the - * payload. - * - * @param fieldDescriptors the descriptors + * it is described but is not present in the payload. * @return descriptors for the fields that are missing from the payload * @throws PayloadHandlingException if a failure occurs */ - List findMissingFields(List fieldDescriptors); + List findMissingFields(); /** * Returns modified content, formatted as a String, that only contains the fields that * are undocumented. A field is undocumented if it is present in the handler's content - * but is not described by the given {@code fieldDescriptors}. If the content is - * completely documented, {@code null} is returned - * - * @param fieldDescriptors the descriptors + * but is not described. If the content is completely documented, {@code null} is + * returned * @return the undocumented content, or {@code null} if all of the content is * documented * @throws PayloadHandlingException if a failure occurs */ - String getUndocumentedContent(List fieldDescriptors); + String getUndocumentedContent(); /** - * Returns the type of the field that is described by the given - * {@code fieldDescriptor} based on the content of the payload. - * - * @param fieldDescriptor the field descriptor - * @return the type of the field + * Create a {@link ContentHandler} for the given content type and payload, described + * by the given descriptors. + * @param content the payload + * @param contentType the content type + * @param descriptors descriptors of the content + * @return the ContentHandler + * @throws PayloadHandlingException if no known ContentHandler can handle the content */ - Object determineFieldType(FieldDescriptor fieldDescriptor); + static ContentHandler forContentWithDescriptors(byte[] content, MediaType contentType, + List descriptors) { + try { + return new JsonContentHandler(content, descriptors); + } + catch (Exception je) { + try { + return new XmlContentHandler(content, descriptors); + } + catch (Exception xe) { + throw new PayloadHandlingException( + "Cannot handle " + contentType + " content as it could not be parsed as JSON or XML"); + } + } + } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index 4c30e63df..3a58b57d9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,8 +45,7 @@ protected FieldDescriptor(String path) { /** * Specifies the type of the field. When documenting a JSON payload, the * {@link JsonFieldType} enumeration will typically be used. - * - * @param type The type of the field + * @param type the type of the field * @return {@code this} * @see JsonFieldType */ @@ -57,7 +56,6 @@ public final FieldDescriptor type(Object type) { /** * Marks the field as optional. - * * @return {@code this} */ public final FieldDescriptor optional() { @@ -67,7 +65,6 @@ public final FieldDescriptor optional() { /** * Returns the path of the field described by this descriptor. - * * @return the path */ public final String getPath() { @@ -76,7 +73,6 @@ public final String getPath() { /** * Returns the type of the field described by this descriptor. - * * @return the type */ public final Object getType() { @@ -85,7 +81,6 @@ public final Object getType() { /** * Returns {@code true} if the described field is optional, otherwise {@code false}. - * * @return {@code true} if the described field is optional, otherwise {@code false} */ public final boolean isOptional() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java index 9fadf5e66..1df61f1a4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDoesNotExistException.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,10 +28,10 @@ public class FieldDoesNotExistException extends RuntimeException { /** * Creates a new {@code FieldDoesNotExistException} that indicates that the field with * the given {@code fieldPath} does not exist. - * * @param fieldPath the path of the field that does not exist */ - public FieldDoesNotExistException(JsonFieldPath fieldPath) { + public FieldDoesNotExistException(String fieldPath) { super("The payload does not contain a field with the path '" + fieldPath + "'"); } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java new file mode 100644 index 000000000..1315ce810 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java @@ -0,0 +1,172 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField; + +/** + * A {@link PayloadSubsectionExtractor} that extracts the subsection of the JSON payload + * identified by a field path. + * + * @author Andy Wilkinson + * @since 1.2.0 + * @see PayloadDocumentation#beneathPath(String) + */ +public class FieldPathPayloadSubsectionExtractor + implements PayloadSubsectionExtractor { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final ObjectMapper prettyPrintingOjectMapper = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + + private final String fieldPath; + + private final String subsectionId; + + /** + * Creates a new {@code FieldPathPayloadSubsectionExtractor} that will extract the + * subsection of the JSON payload beneath the given {@code fieldPath}. The + * {@code fieldPath} prefixed with {@code beneath-} with be used as the subsection ID. + * @param fieldPath the path of the field + */ + protected FieldPathPayloadSubsectionExtractor(String fieldPath) { + this(fieldPath, "beneath-" + fieldPath); + } + + /** + * Creates a new {@code FieldPathPayloadSubsectionExtractor} that will extract the + * subsection of the JSON payload beneath the given {@code fieldPath} and that will + * use the given {@code subsectionId} to identify the subsection. + * @param fieldPath the path of the field + * @param subsectionId the ID of the subsection + */ + protected FieldPathPayloadSubsectionExtractor(String fieldPath, String subsectionId) { + this.fieldPath = fieldPath; + this.subsectionId = subsectionId; + } + + @Override + public byte[] extractSubsection(byte[] payload, MediaType contentType) { + return extractSubsection(payload, contentType, Collections.emptyList()); + } + + @Override + public byte[] extractSubsection(byte[] payload, MediaType contentType, List descriptors) { + try { + ExtractedField extractedField = new JsonFieldProcessor().extract(this.fieldPath, + objectMapper.readValue(payload, Object.class)); + Object value = extractedField.getValue(); + if (value == ExtractedField.ABSENT) { + throw new PayloadHandlingException(this.fieldPath + " does not identify a section of the payload"); + } + Map descriptorsByPath = descriptors.stream() + .collect(Collectors.toMap( + (descriptor) -> JsonFieldPath.compile(this.fieldPath + "." + descriptor.getPath()), + this::prependFieldPath)); + if (value instanceof List) { + List extractedList = (List) value; + if (extractedList.isEmpty()) { + throw new PayloadHandlingException(this.fieldPath + " identifies an empty section of the payload"); + } + JsonContentHandler contentHandler = new JsonContentHandler(payload, descriptorsByPath.values()); + Set uncommonPaths = JsonFieldPaths.from(extractedList) + .getUncommon() + .stream() + .map((path) -> JsonFieldPath + .compile((path.equals("")) ? this.fieldPath : this.fieldPath + "." + path)) + .filter((path) -> { + FieldDescriptor descriptorForPath = descriptorsByPath.getOrDefault(path, + new FieldDescriptor(path.toString())); + return contentHandler.isMissing(descriptorForPath); + }) + .collect(Collectors.toSet()); + if (uncommonPaths.isEmpty()) { + value = extractedList.get(0); + } + else { + String message = this.fieldPath + " identifies multiple sections of " + + "the payload and they do not have a common structure. The " + + "following non-optional uncommon paths were found: "; + message += uncommonPaths.stream() + .map(JsonFieldPath::toString) + .collect(Collectors.toCollection(TreeSet::new)); + throw new PayloadHandlingException(message); + } + } + return getObjectMapper(payload).writeValueAsBytes(value); + } + catch (IOException ex) { + throw new PayloadHandlingException(ex); + } + } + + private FieldDescriptor prependFieldPath(FieldDescriptor original) { + FieldDescriptor prefixed = new FieldDescriptor(this.fieldPath + "." + original.getPath()); + if (original.isOptional()) { + prefixed.optional(); + } + return prefixed; + } + + @Override + public String getSubsectionId() { + return this.subsectionId; + } + + /** + * Returns the path of the field that will be extracted. + * @return the path of the field + */ + protected String getFieldPath() { + return this.fieldPath; + } + + @Override + public FieldPathPayloadSubsectionExtractor withSubsectionId(String subsectionId) { + return new FieldPathPayloadSubsectionExtractor(this.fieldPath, subsectionId); + } + + private ObjectMapper getObjectMapper(byte[] payload) { + if (isPrettyPrinted(payload)) { + return prettyPrintingOjectMapper; + } + return objectMapper; + } + + private boolean isPrettyPrinted(byte[] payload) { + for (byte b : payload) { + if (b == '\n') { + return true; + } + } + return false; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java index b473ef4d1..6b2afd898 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeRequiredException.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,10 +28,10 @@ public class FieldTypeRequiredException extends RuntimeException { /** * Creates a new {@code FieldTypeRequiredException} indicating that a type is required * for the reason described in the given {@code message}. - * * @param message the message */ public FieldTypeRequiredException(String message) { super(message); } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java new file mode 100644 index 000000000..7f574a9e9 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.util.List; + +import org.springframework.http.MediaType; + +/** + * Resolves the type of a field in a request or response payload. + * + * @author Mathias Düsterhöft + * @author Andy Wilkinson + * @since 2.0.3 + */ +public interface FieldTypeResolver { + + /** + * Create a {@code FieldTypeResolver} for the given {@code content} and + * {@code contentType}, described by the given {@code descriptors}. + * @param content the payload that the {@code FieldTypeResolver} should handle + * @param contentType the content type of the payload + * @param descriptors the descriptors of the content + * @return the {@code FieldTypeResolver} + */ + static FieldTypeResolver forContentWithDescriptors(byte[] content, MediaType contentType, + List descriptors) { + return ContentHandler.forContentWithDescriptors(content, contentType, descriptors); + } + + /** + * Resolves the type of the field that is described by the given + * {@code fieldDescriptor} based on the content of the payload. + * @param fieldDescriptor the field descriptor + * @return the type of the field + */ + Object resolveFieldType(FieldDescriptor fieldDescriptor); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypesDoNotMatchException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypesDoNotMatchException.java index a7a5dc202..477a1900b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypesDoNotMatchException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypesDoNotMatchException.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,13 +27,12 @@ class FieldTypesDoNotMatchException extends RuntimeException { /** * Creates a new {@code FieldTypesDoNotMatchException} for the field described by the * given {@code fieldDescriptor} that has the given {@code actualType}. - * * @param fieldDescriptor the field * @param actualType the actual type of the field */ FieldTypesDoNotMatchException(FieldDescriptor fieldDescriptor, Object actualType) { - super("The documented type of the field '" + fieldDescriptor.getPath() + "' is " - + fieldDescriptor.getType() + " but the actual type is " + actualType); + super("The documented type of the field '" + fieldDescriptor.getPath() + "' is " + fieldDescriptor.getType() + + " but the actual type is " + actualType); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index 804cc9a42..52a8d1d48 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -25,34 +26,37 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField; + /** * A {@link ContentHandler} for JSON content. * * @author Andy Wilkinson + * @author Mathias Düsterhöft */ class JsonContentHandler implements ContentHandler { private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); - private final JsonFieldTypeResolver fieldTypeResolver = new JsonFieldTypeResolver(); + private final JsonFieldTypesDiscoverer fieldTypesDiscoverer = new JsonFieldTypesDiscoverer(); - private final ObjectMapper objectMapper = new ObjectMapper() - .enable(SerializationFeature.INDENT_OUTPUT); + private final ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); private final byte[] rawContent; - JsonContentHandler(byte[] content) throws IOException { + private final Collection fieldDescriptors; + + JsonContentHandler(byte[] content, Collection fieldDescriptors) { this.rawContent = content; + this.fieldDescriptors = fieldDescriptors; + readContent(); } @Override - public List findMissingFields( - List fieldDescriptors) { + public List findMissingFields() { List missingFields = new ArrayList<>(); - Object payload = readContent(); - for (FieldDescriptor fieldDescriptor : fieldDescriptors) { - if (!fieldDescriptor.isOptional() && !this.fieldProcessor.hasField( - JsonFieldPath.compile(fieldDescriptor.getPath()), payload)) { + for (FieldDescriptor fieldDescriptor : this.fieldDescriptors) { + if (isMissing(fieldDescriptor)) { missingFields.add(fieldDescriptor); } } @@ -60,12 +64,55 @@ public List findMissingFields( return missingFields; } + boolean isMissing(FieldDescriptor descriptor) { + Object payload = readContent(); + return !descriptor.isOptional() && !this.fieldProcessor.hasField(descriptor.getPath(), payload) + && !isNestedBeneathMissingOptionalField(descriptor, payload); + } + + private boolean isNestedBeneathMissingOptionalField(FieldDescriptor descriptor, Object payload) { + List candidates = new ArrayList<>(this.fieldDescriptors); + candidates.remove(descriptor); + for (FieldDescriptor candidate : candidates) { + if (candidate.isOptional() && descriptor.getPath().startsWith(candidate.getPath()) + && isMissing(candidate, payload)) { + return true; + } + } + return false; + } + + private boolean isMissing(FieldDescriptor candidate, Object payload) { + if (!this.fieldProcessor.hasField(candidate.getPath(), payload)) { + return true; + } + ExtractedField extracted = this.fieldProcessor.extract(candidate.getPath(), payload); + return extracted.getValue() == null || isEmptyCollection(extracted.getValue()); + } + + private boolean isEmptyCollection(Object value) { + if (!(value instanceof Collection)) { + return false; + } + Collection collection = (Collection) value; + for (Object entry : collection) { + if (!isEmptyCollection(entry)) { + return false; + } + } + return true; + } + @Override - public String getUndocumentedContent(List fieldDescriptors) { + public String getUndocumentedContent() { Object content = readContent(); - for (FieldDescriptor fieldDescriptor : fieldDescriptors) { - JsonFieldPath path = JsonFieldPath.compile(fieldDescriptor.getPath()); - this.fieldProcessor.remove(path, content); + for (FieldDescriptor fieldDescriptor : this.fieldDescriptors) { + if (describesSubsection(fieldDescriptor)) { + this.fieldProcessor.removeSubsection(fieldDescriptor.getPath(), content); + } + else { + this.fieldProcessor.remove(fieldDescriptor.getPath(), content); + } } if (!isEmpty(content)) { try { @@ -78,6 +125,10 @@ public String getUndocumentedContent(List fieldDescriptors) { return null; } + private boolean describesSubsection(FieldDescriptor fieldDescriptor) { + return fieldDescriptor instanceof SubsectionDescriptor; + } + private Object readContent() { try { return new ObjectMapper().readValue(this.rawContent, Object.class); @@ -95,22 +146,23 @@ private boolean isEmpty(Object object) { } @Override - public Object determineFieldType(FieldDescriptor fieldDescriptor) { + public Object resolveFieldType(FieldDescriptor fieldDescriptor) { if (fieldDescriptor.getType() == null) { - return this.fieldTypeResolver.resolveFieldType(fieldDescriptor.getPath(), - readContent()); + return this.fieldTypesDiscoverer.discoverFieldTypes(fieldDescriptor.getPath(), readContent()) + .coalesce(fieldDescriptor.isOptional()); } if (!(fieldDescriptor.getType() instanceof JsonFieldType)) { return fieldDescriptor.getType(); } JsonFieldType descriptorFieldType = (JsonFieldType) fieldDescriptor.getType(); try { - JsonFieldType actualFieldType = this.fieldTypeResolver - .resolveFieldType(fieldDescriptor.getPath(), readContent()); - if (descriptorFieldType == JsonFieldType.VARIES - || descriptorFieldType == actualFieldType - || (fieldDescriptor.isOptional() - && actualFieldType == JsonFieldType.NULL)) { + JsonFieldType actualFieldType = this.fieldTypesDiscoverer + .discoverFieldTypes(fieldDescriptor.getPath(), readContent()) + .coalesce(fieldDescriptor.isOptional()); + if (descriptorFieldType == JsonFieldType.VARIES || descriptorFieldType == actualFieldType + || (fieldDescriptor.isOptional() && actualFieldType == JsonFieldType.NULL) + || (isNestedBeneathMissingOptionalField(fieldDescriptor, readContent()) + && actualFieldType == JsonFieldType.VARIES)) { return descriptorFieldType; } throw new FieldTypesDoNotMatchException(fieldDescriptor, actualFieldType); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java index e82bece91..9e7610475 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,44 +27,54 @@ * * @author Andy Wilkinson * @author Jeremy Rickard - * */ final class JsonFieldPath { private static final Pattern BRACKETS_AND_ARRAY_PATTERN = Pattern - .compile("\\[\'(.+?)\'\\]|\\[([0-9]+|\\*){0,1}\\]"); + .compile("\\[\'(.+?)\'\\]|\\[([0-9]+|\\*){0,1}\\]"); - private static final Pattern ARRAY_INDEX_PATTERN = Pattern - .compile("\\[([0-9]+|\\*){0,1}\\]"); + private static final Pattern ARRAY_INDEX_PATTERN = Pattern.compile("\\[([0-9]+|\\*){0,1}\\]"); private final String rawPath; private final List segments; - private final boolean precise; - - private final boolean array; + private final PathType type; - private JsonFieldPath(String rawPath, List segments, boolean precise, - boolean array) { + private JsonFieldPath(String rawPath, List segments, PathType type) { this.rawPath = rawPath; this.segments = segments; - this.precise = precise; - this.array = array; - } - - boolean isPrecise() { - return this.precise; + this.type = type; } - boolean isArray() { - return this.array; + PathType getType() { + return this.type; } List getSegments() { return this.segments; } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + JsonFieldPath other = (JsonFieldPath) obj; + return this.segments.equals(other.segments); + } + + @Override + public int hashCode() { + return this.segments.hashCode(); + } + @Override public String toString() { return this.rawPath; @@ -72,8 +82,7 @@ public String toString() { static JsonFieldPath compile(String path) { List segments = extractSegments(path); - return new JsonFieldPath(path, segments, matchesSingleValue(segments), - isArraySegment(segments.get(segments.size() - 1))); + return new JsonFieldPath(path, segments, matchesSingleValue(segments) ? PathType.SINGLE : PathType.MULTI); } static boolean isArraySegment(String segment) { @@ -83,13 +92,18 @@ static boolean isArraySegment(String segment) { static boolean matchesSingleValue(List segments) { Iterator iterator = segments.iterator(); while (iterator.hasNext()) { - if (isArraySegment(iterator.next()) && iterator.hasNext()) { + String segment = iterator.next(); + if ((isArraySegment(segment) && iterator.hasNext()) || isWildcardSegment(segment)) { return false; } } return true; } + private static boolean isWildcardSegment(String segment) { + return "*".equals(segment); + } + private static List extractSegments(String path) { Matcher matcher = BRACKETS_AND_ARRAY_PATTERN.matcher(path); @@ -98,8 +112,7 @@ private static List extractSegments(String path) { List segments = new ArrayList<>(); while (matcher.find()) { if (previous != matcher.start()) { - segments.addAll(extractDotSeparatedSegments( - path.substring(previous, matcher.start()))); + segments.addAll(extractDotSeparatedSegments(path.substring(previous, matcher.start()))); } if (matcher.group(1) != null) { segments.add(matcher.group(1)); @@ -126,4 +139,22 @@ private static List extractDotSeparatedSegments(String path) { } return segments; } + + /** + * The type of a field path. + */ + enum PathType { + + /** + * The path identifies a single item in the payload. + */ + SINGLE, + + /** + * The path identifies multiple items in the payload. + */ + MULTI + + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPaths.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPaths.java new file mode 100644 index 000000000..3c3251c36 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPaths.java @@ -0,0 +1,97 @@ +/* + * Copyright 2014-2021 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField; + +/** + * {@code JsonFieldPaths} provides support for extracting fields paths from JSON + * structures and identifying uncommon paths. + * + * @author Andy Wilkinson + */ +final class JsonFieldPaths { + + private final Set uncommonFieldPaths; + + private JsonFieldPaths(Set uncommonFieldPaths) { + this.uncommonFieldPaths = uncommonFieldPaths; + } + + Set getUncommon() { + return this.uncommonFieldPaths; + } + + static JsonFieldPaths from(Collection items) { + Set> itemsFieldPaths = new HashSet<>(); + Set allFieldPaths = new HashSet<>(); + for (Object item : items) { + Set paths = new LinkedHashSet<>(); + from(paths, "", item); + itemsFieldPaths.add(paths); + allFieldPaths.addAll(paths); + } + Set uncommonFieldPaths = new HashSet<>(); + for (Set itemFieldPaths : itemsFieldPaths) { + Set uncommonForItem = new HashSet<>(allFieldPaths); + uncommonForItem.removeAll(itemFieldPaths); + uncommonFieldPaths.addAll(uncommonForItem); + } + return new JsonFieldPaths(uncommonFieldPaths); + } + + private static void from(Set paths, String parent, Object object) { + if (object instanceof List) { + String path = append(parent, "[]"); + paths.add(path); + from(paths, path, (List) object); + } + else if (object instanceof Map) { + from(paths, parent, (Map) object); + } + else if (ExtractedField.ABSENT.equals(object)) { + paths.add(parent); + } + } + + private static void from(Set paths, String parent, List items) { + for (Object item : items) { + from(paths, parent, item); + } + } + + private static void from(Set paths, String parent, Map map) { + for (Entry entry : map.entrySet()) { + String path = append(parent, entry.getKey()); + paths.add(path); + from(paths, path, entry.getValue()); + } + } + + private static String append(String path, Object suffix) { + return (path.length() == 0) ? ("" + suffix) : (path + "." + suffix); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index 51bebd813..0fb7ffa72 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,12 @@ package org.springframework.restdocs.payload; import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; + +import org.springframework.restdocs.payload.JsonFieldPath.PathType; /** * A {@code JsonFieldProcessor} processes a payload's fields, allowing them to be @@ -31,100 +33,163 @@ */ final class JsonFieldProcessor { - boolean hasField(JsonFieldPath fieldPath, Object payload) { - final AtomicReference hasField = new AtomicReference<>(false); - traverse(new ProcessingContext(payload, fieldPath), new MatchCallback() { + boolean hasField(String path, Object payload) { + HasFieldMatchCallback callback = new HasFieldMatchCallback(); + traverse(new ProcessingContext(payload, JsonFieldPath.compile(path)), callback); + return callback.fieldFound(); + } + + ExtractedField extract(String path, Object payload) { + JsonFieldPath compiledPath = JsonFieldPath.compile(path); + final List values = new ArrayList<>(); + traverse(new ProcessingContext(payload, compiledPath), new MatchCallback() { @Override public void foundMatch(Match match) { - hasField.set(true); + values.add(match.getValue()); + } + + @Override + public void absent() { + values.add(ExtractedField.ABSENT); } }); - return hasField.get(); + if (values.isEmpty()) { + values.add(ExtractedField.ABSENT); + } + return new ExtractedField((compiledPath.getType() != PathType.SINGLE) ? values : values.get(0), + compiledPath.getType()); } - Object extract(JsonFieldPath path, Object payload) { - final List matches = new ArrayList<>(); - traverse(new ProcessingContext(payload, path), new MatchCallback() { + void remove(String path, Object payload) { + traverse(new ProcessingContext(payload, JsonFieldPath.compile(path)), new MatchCallback() { @Override public void foundMatch(Match match) { - matches.add(match.getValue()); + match.remove(); } }); - if (matches.isEmpty()) { - throw new FieldDoesNotExistException(path); - } - if ((!path.isArray()) && path.isPrecise()) { - return matches.get(0); - } - else { - return matches; - } } - void remove(final JsonFieldPath path, Object payload) { - traverse(new ProcessingContext(payload, path), new MatchCallback() { + void removeSubsection(String path, Object payload) { + traverse(new ProcessingContext(payload, JsonFieldPath.compile(path)), new MatchCallback() { @Override public void foundMatch(Match match) { - match.remove(); + match.removeSubsection(); } }); } private void traverse(ProcessingContext context, MatchCallback matchCallback) { - final String segment = context.getSegment(); + String segment = context.getSegment(); if (JsonFieldPath.isArraySegment(segment)) { - if (context.getPayload() instanceof List) { - handleListPayload(context, matchCallback); + if (context.getPayload() instanceof Collection) { + handleCollectionPayload(context, matchCallback); } } - else if (context.getPayload() instanceof Map - && ((Map) context.getPayload()).containsKey(segment)) { + else if (context.getPayload() instanceof Map) { handleMapPayload(context, matchCallback); } } - private void handleListPayload(ProcessingContext context, - MatchCallback matchCallback) { - List list = context.getPayload(); - final Iterator items = list.iterator(); + private void handleCollectionPayload(ProcessingContext context, MatchCallback matchCallback) { + handleCollectionPayload((Collection) context.getPayload(), matchCallback, context); + } + + private void handleCollectionPayload(Collection collection, MatchCallback matchCallback, + ProcessingContext context) { + if (context.isLeaf()) { + matchCallback.foundMatch(new LeafCollectionMatch(collection, context.getParentMatch())); + } + else { + Iterator items = collection.iterator(); + while (items.hasNext()) { + Object item = items.next(); + traverse(context.descend(item, new CollectionMatch(items, collection, item, context.getParentMatch())), + matchCallback); + } + } + } + + private void handleWildcardPayload(Collection collection, MatchCallback matchCallback, + ProcessingContext context) { + Iterator items = collection.iterator(); if (context.isLeaf()) { while (items.hasNext()) { Object item = items.next(); - matchCallback.foundMatch( - new ListMatch(items, list, item, context.getParentMatch())); + matchCallback.foundMatch(new CollectionMatch(items, collection, item, context.getParentMatch())); } } else { while (items.hasNext()) { Object item = items.next(); - traverse( - context.descend(item, - new ListMatch(items, list, item, context.parent)), + traverse(context.descend(item, new CollectionMatch(items, collection, item, context.getParentMatch())), matchCallback); } } } - private void handleMapPayload(ProcessingContext context, - MatchCallback matchCallback) { + private void handleMapPayload(ProcessingContext context, MatchCallback matchCallback) { Map map = context.getPayload(); - Object item = map.get(context.getSegment()); - MapMatch mapMatch = new MapMatch(item, map, context.getSegment(), - context.getParentMatch()); - if (context.isLeaf()) { - matchCallback.foundMatch(mapMatch); + if (map.containsKey(context.getSegment())) { + Object item = map.get(context.getSegment()); + MapMatch mapMatch = new MapMatch(item, map, context.getSegment(), context.getParentMatch()); + if (context.isLeaf()) { + matchCallback.foundMatch(mapMatch); + } + else { + traverse(context.descend(item, mapMatch), matchCallback); + } + } + else if ("*".equals(context.getSegment())) { + handleWildcardPayload(map.values(), matchCallback, context); } else { - traverse(context.descend(item, mapMatch), matchCallback); + matchCallback.absent(); } } + /** + * {@link MatchCallback} use to determine whether a payload has a particular field. + */ + private static final class HasFieldMatchCallback implements MatchCallback { + + private MatchType matchType = MatchType.NONE; + + @Override + public void foundMatch(Match match) { + this.matchType = this.matchType + .combinedWith((match.getValue() != null) ? MatchType.NON_NULL : MatchType.NULL); + } + + @Override + public void absent() { + this.matchType = this.matchType.combinedWith(MatchType.ABSENT); + } + + boolean fieldFound() { + return this.matchType == MatchType.NON_NULL || this.matchType == MatchType.NULL; + } + + private enum MatchType { + + ABSENT, MIXED, NONE, NULL, NON_NULL; + + MatchType combinedWith(MatchType matchType) { + if (this == NONE || this == matchType) { + return matchType; + } + return MIXED; + } + + } + + } + private static final class MapMatch implements Match { private final Object item; @@ -149,27 +214,55 @@ public Object getValue() { @Override public void remove() { + Object removalCandidate = this.map.get(this.segment); + if (isMapWithEntries(removalCandidate) || isCollectionWithNonScalarEntries(removalCandidate)) { + return; + } this.map.remove(this.segment); if (this.map.isEmpty() && this.parent != null) { this.parent.remove(); } } + @Override + public void removeSubsection() { + this.map.remove(this.segment); + if (this.map.isEmpty() && this.parent != null) { + this.parent.removeSubsection(); + } + } + + private boolean isMapWithEntries(Object object) { + return object instanceof Map && !((Map) object).isEmpty(); + } + + private boolean isCollectionWithNonScalarEntries(Object object) { + if (!(object instanceof Collection)) { + return false; + } + for (Object entry : (Collection) object) { + if (entry instanceof Map || entry instanceof Collection) { + return true; + } + } + return false; + } + } - private static final class ListMatch implements Match { + private static final class CollectionMatch implements Match { private final Iterator items; - private final List list; + private final Collection collection; private final Object item; private final Match parent; - private ListMatch(Iterator items, List list, Object item, Match parent) { + private CollectionMatch(Iterator items, Collection collection, Object item, Match parent) { this.items = items; - this.list = list; + this.collection = collection; this.item = item; this.parent = parent; } @@ -181,18 +274,89 @@ public Object getValue() { @Override public void remove() { + if (!itemIsEmpty()) { + return; + } this.items.remove(); - if (this.list.isEmpty() && this.parent != null) { + if (this.collection.isEmpty() && this.parent != null) { this.parent.remove(); } } + @Override + public void removeSubsection() { + this.items.remove(); + if (this.collection.isEmpty() && this.parent != null) { + this.parent.removeSubsection(); + } + } + + private boolean itemIsEmpty() { + return !isMapWithEntries(this.item) && !isCollectionWithEntries(this.item); + } + + private boolean isMapWithEntries(Object object) { + return object instanceof Map && !((Map) object).isEmpty(); + } + + private boolean isCollectionWithEntries(Object object) { + return object instanceof Collection && !((Collection) object).isEmpty(); + } + + } + + private static final class LeafCollectionMatch implements Match { + + private final Collection collection; + + private final Match parent; + + private LeafCollectionMatch(Collection collection, Match parent) { + this.collection = collection; + this.parent = parent; + } + + @Override + public Collection getValue() { + return this.collection; + } + + @Override + public void remove() { + if (containsOnlyScalars(this.collection)) { + this.collection.clear(); + if (this.parent != null) { + this.parent.remove(); + } + } + } + + @Override + public void removeSubsection() { + this.collection.clear(); + if (this.parent != null) { + this.parent.removeSubsection(); + } + } + + private boolean containsOnlyScalars(Collection collection) { + for (Object item : collection) { + if (item instanceof Collection || item instanceof Map) { + return false; + } + } + return true; + } + } private interface MatchCallback { void foundMatch(Match match); + default void absent() { + } + } private interface Match { @@ -200,6 +364,9 @@ private interface Match { Object getValue(); void remove(); + + void removeSubsection(); + } private static final class ProcessingContext { @@ -216,11 +383,10 @@ private ProcessingContext(Object payload, JsonFieldPath path) { this(payload, path, null, null); } - private ProcessingContext(Object payload, JsonFieldPath path, - List segments, Match parent) { + private ProcessingContext(Object payload, JsonFieldPath path, List segments, Match parent) { this.payload = payload; this.path = path; - this.segments = segments == null ? path.getSegments() : segments; + this.segments = (segments != null) ? segments : path.getSegments(); this.parent = parent; } @@ -242,9 +408,35 @@ private Match getParentMatch() { } private ProcessingContext descend(Object payload, Match match) { - return new ProcessingContext(payload, this.path, - this.segments.subList(1, this.segments.size()), match); + return new ProcessingContext(payload, this.path, this.segments.subList(1, this.segments.size()), match); + } + + } + + /** + * A field that has been extracted from a JSON payload. + */ + static class ExtractedField { + + static final Object ABSENT = new Object(); + + private final Object value; + + private final PathType type; + + ExtractedField(Object value, PathType type) { + this.value = value; + this.type = type; + } + + Object getValue() { + return this.value; } + + PathType getType() { + return this.type; + } + } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java index 2f334c8fb..54b4bb04f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldType.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,4 +66,5 @@ public enum JsonFieldType { public String toString() { return StringUtils.capitalize(this.name().toLowerCase(Locale.ENGLISH)); } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java deleted file mode 100644 index bc7772275..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -package org.springframework.restdocs.payload; - -import java.util.Collection; -import java.util.Map; - -/** - * Resolves the type of a field in a JSON request or response payload. - * - * @author Andy Wilkinson - */ -class JsonFieldTypeResolver { - - private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); - - JsonFieldType resolveFieldType(String path, Object payload) { - JsonFieldPath fieldPath = JsonFieldPath.compile(path); - Object field = this.fieldProcessor.extract(fieldPath, payload); - if (field instanceof Collection && !fieldPath.isPrecise()) { - JsonFieldType commonType = null; - for (Object item : (Collection) field) { - JsonFieldType fieldType = determineFieldType(item); - if (commonType == null) { - commonType = fieldType; - } - else if (fieldType != commonType) { - return JsonFieldType.VARIES; - } - } - return commonType; - } - return determineFieldType(field); - } - - private JsonFieldType determineFieldType(Object fieldValue) { - if (fieldValue == null) { - return JsonFieldType.NULL; - } - if (fieldValue instanceof String) { - return JsonFieldType.STRING; - } - if (fieldValue instanceof Map) { - return JsonFieldType.OBJECT; - } - if (fieldValue instanceof Collection) { - return JsonFieldType.ARRAY; - } - if (fieldValue instanceof Boolean) { - return JsonFieldType.BOOLEAN; - } - return JsonFieldType.NUMBER; - } -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypes.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypes.java new file mode 100644 index 000000000..13d7fc3a5 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypes.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2018 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * {@link JsonFieldType Types} for a field discovered in a JSON payload. + * + * @author Andy Wilkinson + */ +class JsonFieldTypes implements Iterable { + + private final Set fieldTypes; + + JsonFieldTypes(JsonFieldType fieldType) { + this(Collections.singleton(fieldType)); + } + + JsonFieldTypes(Set fieldTypes) { + this.fieldTypes = fieldTypes; + } + + JsonFieldType coalesce(boolean optional) { + Set types = new HashSet<>(this.fieldTypes); + if (optional && types.size() > 1) { + types.remove(JsonFieldType.NULL); + } + if (types.size() == 1) { + return types.iterator().next(); + } + return JsonFieldType.VARIES; + } + + @Override + public Iterator iterator() { + return this.fieldTypes.iterator(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypesDiscoverer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypesDiscoverer.java new file mode 100644 index 000000000..0c683d416 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypesDiscoverer.java @@ -0,0 +1,85 @@ +/* + * Copyright 2014-2018 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.payload.JsonFieldPath.PathType; +import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField; + +/** + * Discovers the types of the fields found at a path in a JSON request or response + * payload. + * + * @author Andy Wilkinson + */ +class JsonFieldTypesDiscoverer { + + private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); + + JsonFieldTypes discoverFieldTypes(String path, Object payload) { + ExtractedField extractedField = this.fieldProcessor.extract(path, payload); + Object value = extractedField.getValue(); + if (value instanceof Collection && extractedField.getType() == PathType.MULTI) { + Collection values = (Collection) value; + if (allAbsent(values)) { + throw new FieldDoesNotExistException(path); + } + Set fieldTypes = new HashSet<>(); + for (Object item : values) { + fieldTypes.add(determineFieldType(item)); + } + return new JsonFieldTypes(fieldTypes); + } + if (value == ExtractedField.ABSENT) { + throw new FieldDoesNotExistException(path); + } + return new JsonFieldTypes(determineFieldType(value)); + } + + private JsonFieldType determineFieldType(Object fieldValue) { + if (fieldValue == null || fieldValue == ExtractedField.ABSENT) { + return JsonFieldType.NULL; + } + if (fieldValue instanceof String) { + return JsonFieldType.STRING; + } + if (fieldValue instanceof Map) { + return JsonFieldType.OBJECT; + } + if (fieldValue instanceof Collection) { + return JsonFieldType.ARRAY; + } + if (fieldValue instanceof Boolean) { + return JsonFieldType.BOOLEAN; + } + return JsonFieldType.NUMBER; + } + + private boolean allAbsent(Collection values) { + for (Object value : values) { + if (value != ExtractedField.ABSENT) { + return false; + } + } + return true; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 2334dc456..4b1c13686 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,7 +68,8 @@ private PayloadDocumentation() { * * The following paths are all present: * - * + *
        + * * * * @@ -94,14 +95,81 @@ private PayloadDocumentation() { * * *
        Paths that are present and their values
        PathValueThe string "three"
        - * - * @param path The path of the field + * @param path the path of the field * @return a {@code FieldDescriptor} ready for further configuration */ public static FieldDescriptor fieldWithPath(String path) { return new FieldDescriptor(path); } + /** + * Creates a {@code FieldDescriptor} that describes a subsection, i.e. a field and all + * of its descendants, with the given {@code path}. + *

        + * When documenting an XML payload, the {@code path} uses XPath, i.e. '/' is used to + * descend to a child node. + *

        + * When documenting a JSON payload, the {@code path} uses '.' to descend into a child + * object and ' {@code []}' to descend into an array. For example, with this JSON + * payload: + * + *

        +	 * {
        +	 *    "a":{
        +	 *        "b":[
        +	 *            {
        +	 *                "c":"one"
        +	 *            },
        +	 *            {
        +	 *                "c":"two"
        +	 *            },
        +	 *            {
        +	 *                "d":"three"
        +	 *            }
        +	 *        ]
        +	 *    }
        +	 * }
        +	 * 
        + * + * The following paths are all present: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
        Paths that are present and their values
        PathValue
        {@code a}An object containing "b"
        {@code a.b}An array containing three objects
        {@code a.b[]}An array containing three objects
        {@code a.b[].c}An array containing the strings "one" and "two"
        {@code a.b[].d}The string "three"
        + *

        + * A subsection descriptor for the array with the path {@code a.b[]} will also + * describe its descendants {@code a.b[].c} and {@code a.b[].d}. + * @param path the path of the subsection + * @return a {@code SubsectionDescriptor} ready for further configuration + */ + public static SubsectionDescriptor subsectionWithPath(String path) { + return new SubsectionDescriptor(path); + } + /** * Returns a {@code Snippet} that will document the fields of the API operations's * request payload. The fields will be documented using the given {@code descriptors}. @@ -110,16 +178,18 @@ public static FieldDescriptor fieldWithPath(String path) { * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the request, * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

        - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. - * + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see FieldDescriptor#description(Object) */ public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) { return requestFields(Arrays.asList(descriptors)); @@ -133,16 +203,17 @@ public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the request, * a failure will also occur. For payloads with a hierarchical structure, documenting - * a field is sufficient for all of its descendants to also be treated as having been - * documented. + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

        - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. - * + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet requestFields(List descriptors) { return new RequestFieldsSnippet(descriptors); @@ -154,13 +225,12 @@ public static RequestFieldsSnippet requestFields(List descripto *

        * If a field is documented, is not marked as optional, and is not present in the * request, a failure will occur. Any undocumented fields will be ignored. - * * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ - public static RequestFieldsSnippet relaxedRequestFields( - FieldDescriptor... descriptors) { + public static RequestFieldsSnippet relaxedRequestFields(FieldDescriptor... descriptors) { return relaxedRequestFields(Arrays.asList(descriptors)); } @@ -170,13 +240,12 @@ public static RequestFieldsSnippet relaxedRequestFields( *

        * If a field is documented, is not marked as optional, and is not present in the * request, a failure will occur. Any undocumented fields will be ignored. - * * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ - public static RequestFieldsSnippet relaxedRequestFields( - List descriptors) { + public static RequestFieldsSnippet relaxedRequestFields(List descriptors) { return new RequestFieldsSnippet(descriptors, true); } @@ -187,22 +256,22 @@ public static RequestFieldsSnippet relaxedRequestFields( *

        * If a field is present in the request payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * field is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

        - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. - * + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * @param attributes the attributes * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ - public static RequestFieldsSnippet requestFields(Map attributes, - FieldDescriptor... descriptors) { + public static RequestFieldsSnippet requestFields(Map attributes, FieldDescriptor... descriptors) { return requestFields(attributes, Arrays.asList(descriptors)); } @@ -213,19 +282,20 @@ public static RequestFieldsSnippet requestFields(Map attributes, *

        * If a field is present in the request payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * field is documented, is not marked as optional, and is not present in the request, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

        - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. - * + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * @param attributes the attributes * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet requestFields(Map attributes, List descriptors) { @@ -239,14 +309,14 @@ public static RequestFieldsSnippet requestFields(Map attributes, *

        * If a field is documented, is not marked as optional, and is not present in the * request, a failure will occur. Any undocumented fields will be ignored. - * * @param attributes the attributes * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ - public static RequestFieldsSnippet relaxedRequestFields( - Map attributes, FieldDescriptor... descriptors) { + public static RequestFieldsSnippet relaxedRequestFields(Map attributes, + FieldDescriptor... descriptors) { return relaxedRequestFields(attributes, Arrays.asList(descriptors)); } @@ -257,221 +327,1215 @@ public static RequestFieldsSnippet relaxedRequestFields( *

        * If a field is documented, is not marked as optional, and is not present in the * request, a failure will occur. Any undocumented fields will be ignored. - * * @param attributes the attributes * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ - public static RequestFieldsSnippet relaxedRequestFields( - Map attributes, List descriptors) { + public static RequestFieldsSnippet relaxedRequestFields(Map attributes, + List descriptors) { return new RequestFieldsSnippet(descriptors, attributes, true); } /** - * Returns a {@code Snippet} that will document the fields of the API operation's - * response payload. The fields will be documented using the given {@code descriptors} - * . + * Returns a {@code Snippet} that will document the fields of the subsection of API + * operations's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors}. *

        - * If a field is present in the response payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * If a field is present in the subsection of the request payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur.For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. *

        - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. - * - * @param descriptors the descriptions of the response payload's fields + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) */ - public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptors) { - return responseFields(Arrays.asList(descriptors)); + public static RequestFieldsSnippet requestFields(PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return requestFields(subsectionExtractor, Arrays.asList(descriptors)); } /** - * Returns a {@code Snippet} that will document the fields of the API operation's - * response payload. The fields will be documented using the given {@code descriptors} - * . + * Returns a {@code Snippet} that will document the fields in the subsection of the + * API operations's request payload extracted by the given {@code subsectionExtractor} + * . The fields will be documented using the given {@code descriptors}. *

        - * If a field is present in the response payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * If a field is present in the subsection of the request payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. *

        - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. - * - * @param descriptors the descriptions of the response payload's fields + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) */ - public static ResponseFieldsSnippet responseFields( + public static RequestFieldsSnippet requestFields(PayloadSubsectionExtractor subsectionExtractor, List descriptors) { - return new ResponseFieldsSnippet(descriptors); + return new RequestFieldsSnippet(subsectionExtractor, descriptors); } /** - * Returns a {@code Snippet} that will document the fields of the API operation's - * response payload. The fields will be documented using the given {@code descriptors} - * . + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operations's request payload extracted by the given {@code subsectionExtractor} + * . The fields will be documented using the given {@code descriptors}. *

        * If a field is documented, is not marked as optional, and is not present in the * request, a failure will occur. Any undocumented fields will be ignored. - * - * @param descriptors the descriptions of the response payload's fields + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) */ - public static ResponseFieldsSnippet relaxedResponseFields( + public static RequestFieldsSnippet relaxedRequestFields(PayloadSubsectionExtractor subsectionExtractor, FieldDescriptor... descriptors) { - return relaxedResponseFields(Arrays.asList(descriptors)); + return relaxedRequestFields(subsectionExtractor, Arrays.asList(descriptors)); } /** - * Returns a {@code Snippet} that will document the fields of the API operation's - * response payload. The fields will be documented using the given {@code descriptors} - * . + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operations's request payload extracted by the given {@code subsectionExtractor} + * . The fields will be documented using the given {@code descriptors}. *

        * If a field is documented, is not marked as optional, and is not present in the * request, a failure will occur. Any undocumented fields will be ignored. - * - * @param descriptors the descriptions of the response payload's fields + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) */ - public static ResponseFieldsSnippet relaxedResponseFields( + public static RequestFieldsSnippet relaxedRequestFields(PayloadSubsectionExtractor subsectionExtractor, List descriptors) { - return new ResponseFieldsSnippet(descriptors, true); + return new RequestFieldsSnippet(subsectionExtractor, descriptors, true); } /** - * Returns a {@code Snippet} that will document the fields of the API operation's - * response payload. The fields will be documented using the given {@code descriptors} - * and the given {@code attributes} will be available during snippet generation. + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors} and the given + * {@code attributes} will be available during snippet generation. *

        - * If a field is present in the response payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * If a field is present in the subsection of the request payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. *

        - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. - * + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param subsectionExtractor the subsection extractor * @param attributes the attributes - * @param descriptors the descriptions of the response payload's fields + * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) */ - public static ResponseFieldsSnippet responseFields(Map attributes, - FieldDescriptor... descriptors) { - return responseFields(attributes, Arrays.asList(descriptors)); + public static RequestFieldsSnippet requestFields(PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return requestFields(subsectionExtractor, attributes, Arrays.asList(descriptors)); } /** - * Returns a {@code Snippet} that will document the fields of the API operation's - * response payload. The fields will be documented using the given {@code descriptors} - * and the given {@code attributes} will be available during snippet generation. + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors} and the given + * {@code attributes} will be available during snippet generation. *

        - * If a field is present in the response payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * payload, a failure will also occur. For payloads with a hierarchical structure, - * documenting a field is sufficient for all of its descendants to also be treated as - * having been documented. + * If a field is present in the subsection of the request payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. *

        - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. - * + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param subsectionExtractor the subsection extractor * @param attributes the attributes - * @param descriptors the descriptions of the response payload's fields + * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) */ - public static ResponseFieldsSnippet responseFields(Map attributes, - List descriptors) { - return new ResponseFieldsSnippet(descriptors, attributes); + public static RequestFieldsSnippet requestFields(PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors, attributes); } /** - * Returns a {@code Snippet} that will document the fields of the API operation's - * response payload. The fields will be documented using the given {@code descriptors} - * and the given {@code attributes} will be available during snippet generation. + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors} and the given + * {@code attributes} will be available during snippet generation. *

        * If a field is documented, is not marked as optional, and is not present in the * request, a failure will occur. Any undocumented fields will be ignored. - * + * @param subsectionExtractor the subsection extractor * @param attributes the attributes - * @param descriptors the descriptions of the response payload's fields + * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) */ - public static ResponseFieldsSnippet relaxedResponseFields( + public static RequestFieldsSnippet relaxedRequestFields(PayloadSubsectionExtractor subsectionExtractor, Map attributes, FieldDescriptor... descriptors) { - return relaxedResponseFields(attributes, Arrays.asList(descriptors)); + return relaxedRequestFields(subsectionExtractor, attributes, Arrays.asList(descriptors)); } /** - * Returns a {@code Snippet} that will document the fields of the API operation's - * response payload. The fields will be documented using the given {@code descriptors} - * and the given {@code attributes} will be available during snippet generation. + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors} and the given + * {@code attributes} will be available during snippet generation. *

        * If a field is documented, is not marked as optional, and is not present in the * request, a failure will occur. Any undocumented fields will be ignored. - * + * @param subsectionExtractor the subsection extractor * @param attributes the attributes - * @param descriptors the descriptions of the response payload's fields + * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) */ - public static ResponseFieldsSnippet relaxedResponseFields( + public static RequestFieldsSnippet relaxedRequestFields(PayloadSubsectionExtractor subsectionExtractor, Map attributes, List descriptors) { - return new ResponseFieldsSnippet(descriptors, attributes, true); + return new RequestFieldsSnippet(subsectionExtractor, descriptors, attributes, true); } /** - * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} - * applied to their paths. - * - * @param pathPrefix the path prefix - * @param descriptors the descriptors to copy - * @return the copied descriptors with the prefix applied + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

        + * If a field is present in the payload of the request part, but is not documented by + * one of the descriptors, a failure will occur when the snippet is invoked. + * Similarly, if a field is documented, is not marked as optional, and is not present + * in the request part's payload, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, FieldDescriptor... descriptors) { + return requestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

        + * If a field is present in the payload of the request part, but is not documented by + * one of the descriptors, a failure will occur when the snippet is invoked. + * Similarly, if a field is documented, is not marked as optional, and is not present + * in the request part's payload, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

        + * If a field is present in the payload of the request part, but is not documented by + * one of the descriptors, a failure will occur when the snippet is invoked. + * Similarly, if a field is documented, is not marked as optional, and is not present + * in the request part's payload, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ - public static List applyPathPrefix(String pathPrefix, + public static RequestPartFieldsSnippet requestPartFields(String part, Map attributes, + FieldDescriptor... descriptors) { + return requestPartFields(part, attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

        + * If a field is present in the payload of the request part, but is not documented by + * one of the descriptors, a failure will occur when the snippet is invoked. + * Similarly, if a field is documented, is not marked as optional, and is not present + * in the request part's payload, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, Map attributes, List descriptors) { - List prefixedDescriptors = new ArrayList<>(); - for (FieldDescriptor descriptor : descriptors) { - FieldDescriptor prefixedDescriptor = new FieldDescriptor( - pathPrefix + descriptor.getPath()) - .description(descriptor.getDescription()) - .type(descriptor.getType()) - .attributes(asArray(descriptor.getAttributes())); - if (descriptor.isIgnored()) { - prefixedDescriptor.ignored(); - } - if (descriptor.isOptional()) { - prefixedDescriptor.optional(); - } - prefixedDescriptors.add(prefixedDescriptor); - } - return prefixedDescriptors; + return new RequestPartFieldsSnippet(part, descriptors, attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, Map attributes, + FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, Map attributes, + List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors, attributes, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

        + * If a field is present in the subsection of the request part payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the subsection's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, FieldDescriptor... descriptors) { + return requestPartFields(part, subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

        + * If a field is present in the subsection of the request part payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the subsection's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the givne {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be available during snippet generation. + *

        + * If a field is present in the subsection of the request part payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, Map attributes, + FieldDescriptor... descriptors) { + return requestPartFields(part, subsectionExtractor, attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be available during snippet generation. + *

        + * If a field is present in the subsection of the request part payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, Map attributes, + List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors, attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be available during snippet generation. + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, Map attributes, + FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, subsectionExtractor, attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be available during snippet generation. + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, Map attributes, + List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors, attributes, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . + *

        + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptors) { + return responseFields(Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . + *

        + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields(List descriptors) { + return new ResponseFieldsSnippet(descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields(FieldDescriptor... descriptors) { + return relaxedResponseFields(Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields(List descriptors) { + return new ResponseFieldsSnippet(descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

        + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static ResponseFieldsSnippet responseFields(Map attributes, FieldDescriptor... descriptors) { + return responseFields(attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

        + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response, + * a failure will also occur. For payloads with a hierarchical structure, documenting + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static ResponseFieldsSnippet responseFields(Map attributes, + List descriptors) { + return new ResponseFieldsSnippet(descriptors, attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields(Map attributes, + FieldDescriptor... descriptors) { + return relaxedResponseFields(attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields(Map attributes, + List descriptors) { + return new ResponseFieldsSnippet(descriptors, attributes, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

        + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field with a {@link #subsectionWithPath(String) subsection + * descriptor} will mean that all of its descendants are also treated as having been + * documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields(PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return responseFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

        + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field with a {@link #subsectionWithPath(String) subsection + * descriptor} will mean that all of its descendants are also treated as having been + * documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields(PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields(PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return relaxedResponseFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields(PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be available during + * snippet generation. + *

        + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field with a {@link #subsectionWithPath(String) subsection + * descriptor} will mean that all of its descendants are also treated as having been + * documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields(PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return responseFields(subsectionExtractor, attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be available during + * snippet generation. + *

        + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, a failure will also occur. For payloads with a hierarchical structure, + * documenting a field with a {@link #subsectionWithPath(String) subsection + * descriptor} will mean that all of its descendants are also treated as having been + * documented. + *

        + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields(PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors, attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be available during + * snippet generation. + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields(PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return relaxedResponseFields(subsectionExtractor, attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be available during + * snippet generation. + *

        + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields(PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors, attributes, true); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * request payload. + * @return the snippet that will document the request body + */ + public static RequestBodySnippet requestBody() { + return new RequestBodySnippet(); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * request payload. The given attributes will be made available during snippet + * generation. + * @param attributes the attributes + * @return the snippet that will document the request body + */ + public static RequestBodySnippet requestBody(Map attributes) { + return new RequestBodySnippet(attributes); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's request payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. + * @param subsectionExtractor the subsection extractor + * @return the snippet that will document the request body subsection + */ + public static RequestBodySnippet requestBody(PayloadSubsectionExtractor subsectionExtractor) { + return new RequestBodySnippet(subsectionExtractor); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's request payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The given attributes will be made available during + * snippet generation. + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @return the snippet that will document the request body subsection + */ + public static RequestBodySnippet requestBody(PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + return new RequestBodySnippet(subsectionExtractor, attributes); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * response payload. + * @return the snippet that will document the response body + */ + public static ResponseBodySnippet responseBody() { + return new ResponseBodySnippet(); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * response payload. The given attributes will be made available during snippet + * generation. + * @param attributes the attributes + * @return the snippet that will document the response body + */ + public static ResponseBodySnippet responseBody(Map attributes) { + return new ResponseBodySnippet(attributes); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. + * @param subsectionExtractor the subsection extractor + * @return the snippet that will document the response body subsection + */ + public static ResponseBodySnippet responseBody(PayloadSubsectionExtractor subsectionExtractor) { + return new ResponseBodySnippet(subsectionExtractor); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The given attributes will be made available during + * snippet generation. + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @return the snippet that will document the response body subsection + */ + public static ResponseBodySnippet responseBody(PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + return new ResponseBodySnippet(subsectionExtractor, attributes); + } + + /** + * Returns a {@code Snippet} that will document the body of specified part of the API + * operation's request payload. + * @param partName the name of the request part + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName) { + return new RequestPartBodySnippet(partName); + } + + /** + * Returns a {@code Snippet} that will document the body of specified part of the API + * operation's request payload. The given attributes will be made available during + * snippet generation. + * @param partName the name of the request part + * @param attributes the attributes + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName, Map attributes) { + return new RequestPartBodySnippet(partName, attributes); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of specified + * part of the API operation's request payload. The subsection will be extracted using + * the given {@code subsectionExtractor}. + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName, + PayloadSubsectionExtractor subsectionExtractor) { + return new RequestPartBodySnippet(partName, subsectionExtractor); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of specified + * part of the API operation's request payload. The subsection will be extracted using + * the given {@code subsectionExtractor}. The given attributes will be made available + * during snippet generation. + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName, + PayloadSubsectionExtractor subsectionExtractor, Map attributes) { + return new RequestPartBodySnippet(partName, subsectionExtractor, attributes); + } + + /** + * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} + * applied to their paths. + * @param pathPrefix the path prefix + * @param descriptors the descriptors to copy + * @return the copied descriptors with the prefix applied + */ + public static List applyPathPrefix(String pathPrefix, List descriptors) { + List prefixedDescriptors = new ArrayList<>(); + for (FieldDescriptor descriptor : descriptors) { + String prefixedPath = pathPrefix + descriptor.getPath(); + FieldDescriptor prefixedDescriptor = (descriptor instanceof SubsectionDescriptor) + ? new SubsectionDescriptor(prefixedPath) : new FieldDescriptor(prefixedPath); + prefixedDescriptor.description(descriptor.getDescription()) + .type(descriptor.getType()) + .attributes(asArray(descriptor.getAttributes())); + if (descriptor.isIgnored()) { + prefixedDescriptor.ignored(); + } + if (descriptor.isOptional()) { + prefixedDescriptor.optional(); + } + prefixedDescriptors.add(prefixedDescriptor); + } + return prefixedDescriptors; + } + + /** + * Returns a {@link PayloadSubsectionExtractor} that will extract the subsection of + * the JSON payload found beneath the given {@code path}. + * @param path the path + * @return the subsection extractor + * @since 1.2.0 + */ + public static PayloadSubsectionExtractor beneathPath(String path) { + return new FieldPathPayloadSubsectionExtractor(path); } private static Attribute[] asArray(Map attributeMap) { List attributes = new ArrayList<>(); for (Map.Entry attribute : attributeMap.entrySet()) { - attributes - .add(Attributes.key(attribute.getKey()).value(attribute.getValue())); + attributes.add(Attributes.key(attribute.getKey()).value(attribute.getValue())); } return attributes.toArray(new Attribute[attributes.size()]); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java index f2cd61176..6098e01ae 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,16 @@ class PayloadHandlingException extends RuntimeException { /** - * Creates a new {@code PayloadHandlingException} with the given cause. + * Creates a new {@code PayloadHandlingException} with the given {@code message}. + * @param message the message + * @since 1.2.0 + */ + PayloadHandlingException(String message) { + super(message); + } + + /** + * Creates a new {@code PayloadHandlingException} with the given {@code cause}. * @param cause the cause of the failure */ PayloadHandlingException(Throwable cause) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java new file mode 100644 index 000000000..097a8125f --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java @@ -0,0 +1,67 @@ +/* + * Copyright 2014-2018 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.util.List; + +import org.springframework.http.MediaType; + +/** + * Strategy interface for extracting a subsection of a payload. + * + * @param the subsection extractor subclass + * @author Andy Wilkinson + * @since 1.2.0 + */ +public interface PayloadSubsectionExtractor> { + + /** + * Extracts a subsection of the given {@code payload} that has the given + * {@code contentType}. + * @param payload the payload + * @param contentType the content type of the payload + * @return the subsection of the payload + */ + byte[] extractSubsection(byte[] payload, MediaType contentType); + + /** + * Extracts a subsection of the given {@code payload} that has the given + * {@code contentType} and that is described by the given {@code descriptors}. + * @param payload the payload + * @param contentType the content type of the payload + * @param descriptors descriptors that describe the payload + * @return the subsection of the payload + * @since 2.0.4 + */ + default byte[] extractSubsection(byte[] payload, MediaType contentType, List descriptors) { + return extractSubsection(payload, contentType); + } + + /** + * Returns an identifier for the subsection that this extractor will extract. + * @return the identifier + */ + String getSubsectionId(); + + /** + * Returns an extractor with the given {@code subsectionId}. + * @param subsectionId the subsection ID + * @return the customized extractor + */ + T withSubsectionId(String subsectionId); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java new file mode 100644 index 000000000..bdbb4b401 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java @@ -0,0 +1,80 @@ +/* + * Copyright 2014-2019 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the body of a request. + * + * @author Andy Wilkinson + */ +public class RequestBodySnippet extends AbstractBodySnippet { + + /** + * Creates a new {@code RequestBodySnippet}. + */ + public RequestBodySnippet() { + this(null, null); + } + + /** + * Creates a new {@code RequestBodySnippet} that will document the subsection of the + * request body extracted by the given {@code subsectionExtractor}. + * @param subsectionExtractor the subsection extractor + */ + public RequestBodySnippet(PayloadSubsectionExtractor subsectionExtractor) { + this(subsectionExtractor, null); + } + + /** + * Creates a new {@code RequestBodySnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * @param attributes the additional attributes + */ + public RequestBodySnippet(Map attributes) { + this(null, attributes); + } + + /** + * Creates a new {@code RequestBodySnippet} that will document the subsection of the + * request body extracted by the given {@code subsectionExtractor}. The given + * additional {@code attributes} that will be included in the model during template + * rendering. + * @param subsectionExtractor the subsection extractor + * @param attributes the additional attributes + */ + public RequestBodySnippet(PayloadSubsectionExtractor subsectionExtractor, Map attributes) { + super("request", subsectionExtractor, attributes); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return operation.getRequest().getContent(); + } + + @Override + protected MediaType getContentType(Operation operation) { + return operation.getRequest().getHeaders().getContentType(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index beff39d7c..63a39272d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,6 @@ public class RequestFieldsSnippet extends AbstractFieldsSnippet { * Creates a new {@code RequestFieldsSnippet} that will document the fields in the * request using the given {@code descriptors}. Undocumented fields will trigger a * failure. - * * @param descriptors the descriptors */ protected RequestFieldsSnippet(List descriptors) { @@ -50,12 +49,10 @@ protected RequestFieldsSnippet(List descriptors) { * Creates a new {@code RequestFieldsSnippet} that will document the fields in the * request using the given {@code descriptors}. If {@code ignoreUndocumentedFields} is * {@code true}, undocumented fields will be ignored and will not trigger a failure. - * * @param descriptors the descriptors * @param ignoreUndocumentedFields whether undocumented fields should be ignored */ - protected RequestFieldsSnippet(List descriptors, - boolean ignoreUndocumentedFields) { + protected RequestFieldsSnippet(List descriptors, boolean ignoreUndocumentedFields) { this(descriptors, null, ignoreUndocumentedFields); } @@ -64,12 +61,10 @@ protected RequestFieldsSnippet(List descriptors, * request using the given {@code descriptors}. The given {@code attributes} will be * included in the model during template rendering. Undocumented fields will trigger a * failure. - * * @param descriptors the descriptors * @param attributes the additional attributes */ - protected RequestFieldsSnippet(List descriptors, - Map attributes) { + protected RequestFieldsSnippet(List descriptors, Map attributes) { this(descriptors, attributes, false); } @@ -79,14 +74,74 @@ protected RequestFieldsSnippet(List descriptors, * included in the model during template rendering. If * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be * ignored and will not trigger a failure. - * * @param descriptors the descriptors * @param attributes the additional attributes * @param ignoreUndocumentedFields whether undocumented fields should be ignored */ - protected RequestFieldsSnippet(List descriptors, + protected RequestFieldsSnippet(List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { + this(null, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. Undocumented fields will trigger a failure. + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + this(subsectionExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. If {@code ignoreUndocumentedFields} is {@code true}, + * undocumented fields will be ignored and will not trigger a failure. + * @param subsectionExtractor the subsection extractor document + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, List descriptors, + boolean ignoreUndocumentedFields) { + this(subsectionExtractor, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. The given {@code attributes} will be included in the + * model during template rendering. Undocumented fields will trigger a failure. + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, List descriptors, + Map attributes) { + this(subsectionExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. The given {@code attributes} will be included in the + * model during template rendering. If {@code ignoreUndocumentedFields} is + * {@code true}, undocumented fields will be ignored and will not trigger a failure. + * @param subsectionExtractor the path identifying the subsection of the payload to + * document + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super("request", descriptors, attributes, ignoreUndocumentedFields); + super("request", descriptors, attributes, ignoreUndocumentedFields, subsectionExtractor); } @Override @@ -103,7 +158,6 @@ protected byte[] getContent(Operation operation) throws IOException { * Returns a new {@code RequestFieldsSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. - * * @param additionalDescriptors the additional descriptors * @return the new snippet */ @@ -115,7 +169,6 @@ public final RequestFieldsSnippet and(FieldDescriptor... additionalDescriptors) * Returns a new {@code RequestFieldsSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. - * * @param additionalDescriptors the additional descriptors * @return the new snippet */ @@ -128,18 +181,17 @@ public final RequestFieldsSnippet and(List additionalDescriptor * attributes and its descriptors combined with the given * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path * of each additional descriptor. - * * @param pathPrefix the prefix to apply to the additional descriptors * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public final RequestFieldsSnippet andWithPrefix(String pathPrefix, - FieldDescriptor... additionalDescriptors) { + public final RequestFieldsSnippet andWithPrefix(String pathPrefix, FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll( - PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); - return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); + combinedDescriptors + .addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + return new RequestFieldsSnippet(getSubsectionExtractor(), combinedDescriptors, getAttributes(), + isIgnoredUndocumentedFields()); } /** @@ -147,18 +199,15 @@ public final RequestFieldsSnippet andWithPrefix(String pathPrefix, * attributes and its descriptors combined with the given * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path * of each additional descriptor. - * * @param pathPrefix the prefix to apply to the additional descriptors * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public final RequestFieldsSnippet andWithPrefix(String pathPrefix, - List additionalDescriptors) { - List combinedDescriptors = new ArrayList<>( - getFieldDescriptors()); - combinedDescriptors.addAll( - PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); - return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); + public final RequestFieldsSnippet andWithPrefix(String pathPrefix, List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(getFieldDescriptors()); + combinedDescriptors.addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); + return new RequestFieldsSnippet(getSubsectionExtractor(), combinedDescriptors, getAttributes(), + isIgnoredUndocumentedFields()); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java new file mode 100644 index 000000000..3ab046015 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java @@ -0,0 +1,103 @@ +/* + * Copyright 2014-2019 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; + +/** + * A {@link Snippet} that documents the body of a request part. + * + * @author Andy Wilkinson + */ +public class RequestPartBodySnippet extends AbstractBodySnippet { + + private final String partName; + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the body of the + * request part with the given {@code partName}. + * @param partName the name of the request part + */ + public RequestPartBodySnippet(String partName) { + this(partName, null, null); + } + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the subsection of + * the body of the request part with the given {@code partName} extracted by the given + * {@code subsectionExtractor}. + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + */ + public RequestPartBodySnippet(String partName, PayloadSubsectionExtractor subsectionExtractor) { + this(partName, subsectionExtractor, null); + } + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the body of the + * request part with the given {@code partName}. The given additional + * {@code attributes} will be included in the model during template rendering. + * @param partName the name of the request part + * @param attributes the additional attributes + */ + public RequestPartBodySnippet(String partName, Map attributes) { + this(partName, null, attributes); + } + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the body of the + * request part with the given {@code partName}. The subsection of the body extracted + * by the given {@code subsectionExtractor} will be documented and the given + * additional {@code attributes} that will be included in the model during template + * rendering. + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + * @param attributes the additional attributes + */ + public RequestPartBodySnippet(String partName, PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super("request-part-" + partName, "request-part", subsectionExtractor, attributes); + this.partName = partName; + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return findPart(operation).getContent(); + } + + @Override + protected MediaType getContentType(Operation operation) { + return findPart(operation).getHeaders().getContentType(); + } + + private OperationRequestPart findPart(Operation operation) { + for (OperationRequestPart candidate : operation.getRequest().getParts()) { + if (candidate.getName().equals(this.partName)) { + return candidate; + } + } + throw new SnippetException("A request part named '" + this.partName + "' was not found in the request"); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java new file mode 100644 index 000000000..eb7d9aa57 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java @@ -0,0 +1,239 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; + +/** + * A {@link Snippet} that documents the fields in a request part. + * + * @author Mathieu Pousse + * @author Andy Wilkinson + * @since 1.2.0 + * @see PayloadDocumentation#requestPartFields(String, FieldDescriptor...) + * @see PayloadDocumentation#requestPartFields(String, List) + */ +public class RequestPartFieldsSnippet extends AbstractFieldsSnippet { + + private final String partName; + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in the + * request part using the given {@code descriptors}. Undocumented fields will trigger + * a failure. + * @param partName the part name + * @param descriptors the descriptors + */ + protected RequestPartFieldsSnippet(String partName, List descriptors) { + this(partName, descriptors, null, false); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in the + * request part using the given {@code descriptors}. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * @param partName the part name + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, List descriptors, + boolean ignoreUndocumentedFields) { + this(partName, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. Undocumented fields will trigger a + * failure. + * @param partName the part name + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestPartFieldsSnippet(String partName, List descriptors, + Map attributes) { + this(partName, descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * @param partName the part name + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { + this(partName, null, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection of the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. Undocumented fields + * will trigger a failure. + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + */ + protected RequestPartFieldsSnippet(String partName, PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + this(partName, subsectionExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, PayloadSubsectionExtractor subsectionExtractor, + List descriptors, boolean ignoreUndocumentedFields) { + this(partName, subsectionExtractor, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection of the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. + * Undocumented fields will trigger a failure. + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestPartFieldsSnippet(String partName, PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes) { + this(partName, subsectionExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection of the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes, boolean ignoreUndocumentedFields) { + super("request-part-" + partName, "request-part", descriptors, attributes, ignoreUndocumentedFields, + subsectionExtractor); + this.partName = partName; + } + + @Override + protected MediaType getContentType(Operation operation) { + return findPart(operation).getHeaders().getContentType(); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return findPart(operation).getContent(); + } + + private OperationRequestPart findPart(Operation operation) { + for (OperationRequestPart candidate : operation.getRequest().getParts()) { + if (candidate.getName().equals(this.partName)) { + return candidate; + } + } + throw new SnippetException("A request part named '" + this.partName + "' was not found in the request"); + } + + /** + * Returns a new {@code RequestPartFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code RequestPartFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartFieldsSnippet and(List additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, FieldDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(getFieldDescriptors()); + combinedDescriptors + .addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, this.getAttributes()); + } + + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(getFieldDescriptors()); + combinedDescriptors.addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); + return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, this.getAttributes()); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java new file mode 100644 index 000000000..c316b54fa --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java @@ -0,0 +1,80 @@ +/* + * Copyright 2014-2019 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the body of a response. + * + * @author Andy Wilkinson + */ +public class ResponseBodySnippet extends AbstractBodySnippet { + + /** + * Creates a new {@code ResponseBodySnippet}. + */ + public ResponseBodySnippet() { + this(null, null); + } + + /** + * Creates a new {@code ResponseBodySnippet} that will document the subsection of the + * response body extracted by the given {@code subsectionExtractor}. + * @param subsectionExtractor the subsection extractor + */ + public ResponseBodySnippet(PayloadSubsectionExtractor subsectionExtractor) { + this(subsectionExtractor, null); + } + + /** + * Creates a new {@code ResponseBodySnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * @param attributes the additional attributes + */ + public ResponseBodySnippet(Map attributes) { + this(null, attributes); + } + + /** + * Creates a new {@code ResponseBodySnippet} that will document the subsection of the + * response body extracted by the given {@code subsectionExtractor}. The given + * additional {@code attributes} that will be included in the model during template + * rendering. + * @param subsectionExtractor the subsection extractor + * @param attributes the additional attributes + */ + public ResponseBodySnippet(PayloadSubsectionExtractor subsectionExtractor, Map attributes) { + super("response", subsectionExtractor, attributes); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return operation.getResponse().getContent(); + } + + @Override + protected MediaType getContentType(Operation operation) { + return operation.getResponse().getHeaders().getContentType(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 33d9aaf14..5e452b7e2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,6 @@ public class ResponseFieldsSnippet extends AbstractFieldsSnippet { * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the * response using the given {@code descriptors}. Undocumented fields will trigger a * failure. - * * @param descriptors the descriptors */ protected ResponseFieldsSnippet(List descriptors) { @@ -51,12 +50,10 @@ protected ResponseFieldsSnippet(List descriptors) { * response using the given {@code descriptors}. If {@code ignoreUndocumentedFields} * is {@code true}, undocumented fields will be ignored and will not trigger a * failure. - * * @param descriptors the descriptors * @param ignoreUndocumentedFields whether undocumented fields should be ignored */ - protected ResponseFieldsSnippet(List descriptors, - boolean ignoreUndocumentedFields) { + protected ResponseFieldsSnippet(List descriptors, boolean ignoreUndocumentedFields) { this(descriptors, null, ignoreUndocumentedFields); } @@ -65,12 +62,10 @@ protected ResponseFieldsSnippet(List descriptors, * response using the given {@code descriptors}. The given {@code attributes} will be * included in the model during template rendering. Undocumented fields will trigger a * failure. - * * @param descriptors the descriptors * @param attributes the additional attributes */ - protected ResponseFieldsSnippet(List descriptors, - Map attributes) { + protected ResponseFieldsSnippet(List descriptors, Map attributes) { this(descriptors, attributes, false); } @@ -80,14 +75,77 @@ protected ResponseFieldsSnippet(List descriptors, * included in the model during template rendering. If * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be * ignored and will not trigger a failure. - * * @param descriptors the descriptors * @param attributes the additional attributes * @param ignoreUndocumentedFields whether undocumented fields should be ignored */ - protected ResponseFieldsSnippet(List descriptors, - Map attributes, boolean ignoreUndocumentedFields) { - super("response", descriptors, attributes, ignoreUndocumentedFields); + protected ResponseFieldsSnippet(List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { + this(null, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in a + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. Undocumented fields will + * trigger a failure. + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + this(subsectionExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, boolean ignoreUndocumentedFields) { + this(subsectionExtractor, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in a + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. + * Undocumented fields will trigger a failure. + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes) { + this(subsectionExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in a + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes, boolean ignoreUndocumentedFields) { + super("response", descriptors, attributes, ignoreUndocumentedFields, subsectionExtractor); } @Override @@ -104,7 +162,6 @@ protected byte[] getContent(Operation operation) throws IOException { * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. - * * @param additionalDescriptors the additional descriptors * @return the new snippet */ @@ -116,7 +173,6 @@ public final ResponseFieldsSnippet and(FieldDescriptor... additionalDescriptors) * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. - * * @param additionalDescriptors the additional descriptors * @return the new snippet */ @@ -129,18 +185,17 @@ public final ResponseFieldsSnippet and(List additionalDescripto * attributes and its descriptors combined with the given * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path * of each additional descriptor. - * * @param pathPrefix the prefix to apply to the additional descriptors * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, - FieldDescriptor... additionalDescriptors) { + public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll( - PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); - return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); + combinedDescriptors + .addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + return new ResponseFieldsSnippet(getSubsectionExtractor(), combinedDescriptors, this.getAttributes(), + isIgnoredUndocumentedFields()); } /** @@ -148,18 +203,15 @@ public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, * attributes and its descriptors combined with the given * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path * of each additional descriptor. - * * @param pathPrefix the prefix to apply to the additional descriptors * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, - List additionalDescriptors) { - List combinedDescriptors = new ArrayList<>( - getFieldDescriptors()); - combinedDescriptors.addAll( - PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); - return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); + public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(getFieldDescriptors()); + combinedDescriptors.addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); + return new ResponseFieldsSnippet(getSubsectionExtractor(), combinedDescriptors, this.getAttributes(), + isIgnoredUndocumentedFields()); } } diff --git a/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/SubsectionDescriptor.java similarity index 57% rename from samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/SubsectionDescriptor.java index a6f0fddc1..5dc16c521 100644 --- a/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/SubsectionDescriptor.java @@ -14,23 +14,24 @@ * limitations under the License. */ -package com.example +package org.springframework.restdocs.payload; -import grails.rest.Resource - -@Resource(uri='/tags', formats = ['json', 'xml']) -class Tag { - - Long id - - String name - - static hasMany = [notes: Note] - - static belongsTo = Note - - static mapping = { - notes joinTable: [name: "mm_notes_tags", key: 'mm_tag_id'] +/** + * A description of a subsection, i.e. a field and all of its descendants, in a request or + * response payload. + * + * @author Andy Wilkinson + * @since 1.2.0 + */ +public class SubsectionDescriptor extends FieldDescriptor { + + /** + * Creates a new {@code SubsectionDescriptor} describing the subsection with the given + * {@code path}. + * @param path the path + */ + protected SubsectionDescriptor(String path) { + super(path); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index e8ffceb2c..8ca819900 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.io.ByteArrayInputStream; import java.io.StringWriter; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import javax.xml.parsers.DocumentBuilder; @@ -36,6 +37,7 @@ import org.w3c.dom.Attr; import org.w3c.dom.Document; +import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; @@ -51,23 +53,25 @@ class XmlContentHandler implements ContentHandler { private final byte[] rawContent; - XmlContentHandler(byte[] rawContent) { + private final List fieldDescriptors; + + XmlContentHandler(byte[] rawContent, List fieldDescriptors) { try { - this.documentBuilder = DocumentBuilderFactory.newInstance() - .newDocumentBuilder(); + this.documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); } catch (ParserConfigurationException ex) { throw new IllegalStateException("Failed to create document builder", ex); } this.rawContent = rawContent; + this.fieldDescriptors = fieldDescriptors; + readPayload(); } @Override - public List findMissingFields( - List fieldDescriptors) { + public List findMissingFields() { List missingFields = new ArrayList<>(); Document payload = readPayload(); - for (FieldDescriptor fieldDescriptor : fieldDescriptors) { + for (FieldDescriptor fieldDescriptor : this.fieldDescriptors) { if (!fieldDescriptor.isOptional()) { NodeList matchingNodes = findMatchingNodes(fieldDescriptor, payload); if (matchingNodes.getLength() == 0) { @@ -79,11 +83,9 @@ public List findMissingFields( return missingFields; } - private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, - Document payload) { + private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, Document payload) { try { - return (NodeList) createXPath(fieldDescriptor.getPath()).evaluate(payload, - XPathConstants.NODESET); + return (NodeList) createXPath(fieldDescriptor.getPath()).evaluate(payload, XPathConstants.NODESET); } catch (XPathExpressionException ex) { throw new PayloadHandlingException(ex); @@ -92,27 +94,26 @@ private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, private Document readPayload() { try { - return this.documentBuilder - .parse(new InputSource(new ByteArrayInputStream(this.rawContent))); + return this.documentBuilder.parse(new InputSource(new ByteArrayInputStream(this.rawContent))); } catch (Exception ex) { throw new PayloadHandlingException(ex); } } - private XPathExpression createXPath(String fieldPath) - throws XPathExpressionException { + private XPathExpression createXPath(String fieldPath) throws XPathExpressionException { return XPathFactory.newInstance().newXPath().compile(fieldPath); } @Override - public String getUndocumentedContent(List fieldDescriptors) { + public String getUndocumentedContent() { Document payload = readPayload(); - for (FieldDescriptor fieldDescriptor : fieldDescriptors) { + List matchedButNotRemoved = new ArrayList<>(); + for (FieldDescriptor fieldDescriptor : this.fieldDescriptors) { NodeList matchingNodes; try { - matchingNodes = (NodeList) createXPath(fieldDescriptor.getPath()) - .evaluate(payload, XPathConstants.NODESET); + matchingNodes = (NodeList) createXPath(fieldDescriptor.getPath()).evaluate(payload, + XPathConstants.NODESET); } catch (XPathExpressionException ex) { throw new PayloadHandlingException(ex); @@ -124,17 +125,49 @@ public String getUndocumentedContent(List fieldDescriptors) { attr.getOwnerElement().removeAttributeNode(attr); } else { - node.getParentNode().removeChild(node); + if (fieldDescriptor instanceof SubsectionDescriptor || isLeafNode(node)) { + node.getParentNode().removeChild(node); + } + else { + matchedButNotRemoved.add(node); + } } } } + removeLeafNodes(matchedButNotRemoved); if (payload.getChildNodes().getLength() > 0) { return prettyPrint(payload); } return null; } + private void removeLeafNodes(List candidates) { + boolean changed = true; + while (changed) { + changed = false; + Iterator iterator = candidates.iterator(); + while (iterator.hasNext()) { + Node node = iterator.next(); + if (isLeafNode(node)) { + node.getParentNode().removeChild(node); + iterator.remove(); + changed = true; + } + } + } + } + + private boolean isLeafNode(Node node) { + NodeList childNodes = node.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + if (childNodes.item(i) instanceof Element) { + return false; + } + } + return true; + } + private String prettyPrint(Document document) { try { StringWriter stringWriter = new StringWriter(); @@ -153,7 +186,7 @@ private String prettyPrint(Document document) { } @Override - public Object determineFieldType(FieldDescriptor fieldDescriptor) { + public Object resolveFieldType(FieldDescriptor fieldDescriptor) { if (fieldDescriptor.getType() != null) { return fieldDescriptor.getType(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 5465c5ca5..cce7a7d1a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,49 +43,26 @@ public abstract class AbstractParametersSnippet extends TemplatedSnippet { private final boolean ignoreUndocumentedParameters; - /** - * Creates a new {@code AbstractParametersSnippet} that will produce a snippet with - * the given {@code snippetName} that will document parameters using the given - * {@code descriptors}. The given {@code attributes} will be included in the model - * during template rendering. Undocumented parameters will trigger a failure. - * - * @param snippetName The snippet name - * @param descriptors The descriptors - * @param attributes The additional attributes - * @deprecated since 1.1 in favour of - * {@link #AbstractParametersSnippet(String, List, Map, boolean)} - */ - @Deprecated - protected AbstractParametersSnippet(String snippetName, - List descriptors, Map attributes) { - this(snippetName, descriptors, attributes, false); - } - /** * Creates a new {@code AbstractParametersSnippet} that will produce a snippet with * the given {@code snippetName} that will document parameters using the given * {@code descriptors}. The given {@code attributes} will be included in the model * during template rendering. If {@code ignoreUndocumentedParameters} is {@code true}, * undocumented parameters will be ignored and will not trigger a failure. - * - * @param snippetName The snippet name - * @param descriptors The descriptors - * @param attributes The additional attributes + * @param snippetName the snippet name + * @param descriptors the descriptors + * @param attributes the additional attributes * @param ignoreUndocumentedParameters whether undocumented parameters should be * ignored */ - protected AbstractParametersSnippet(String snippetName, - List descriptors, Map attributes, - boolean ignoreUndocumentedParameters) { + protected AbstractParametersSnippet(String snippetName, List descriptors, + Map attributes, boolean ignoreUndocumentedParameters) { super(snippetName, attributes); for (ParameterDescriptor descriptor : descriptors) { - Assert.notNull(descriptor.getName(), - "Parameter descriptors must have a name"); + Assert.notNull(descriptor.getName(), "Parameter descriptors must have a name"); if (!descriptor.isIgnored()) { - Assert.notNull(descriptor.getDescription(), - "The descriptor for parameter '" + descriptor.getName() - + "' must either have a description or be marked as " - + "ignored"); + Assert.notNull(descriptor.getDescription(), "The descriptor for parameter '" + descriptor.getName() + + "' must either have a description or be marked as " + "ignored"); } this.descriptorsByName.put(descriptor.getName(), descriptor); } @@ -98,8 +75,7 @@ protected Map createModel(Operation operation) { Map model = new HashMap<>(); List> parameters = new ArrayList<>(); - for (Entry entry : this.descriptorsByName - .entrySet()) { + for (Entry entry : this.descriptorsByName.entrySet()) { ParameterDescriptor descriptor = entry.getValue(); if (!descriptor.isIgnored()) { parameters.add(createModelForDescriptor(descriptor)); @@ -112,8 +88,7 @@ protected Map createModel(Operation operation) { private void verifyParameterDescriptors(Operation operation) { Set actualParameters = extractActualParameters(operation); Set expectedParameters = new HashSet<>(); - for (Entry entry : this.descriptorsByName - .entrySet()) { + for (Entry entry : this.descriptorsByName.entrySet()) { if (!entry.getValue().isOptional()) { expectedParameters.add(entry.getKey()); } @@ -137,7 +112,6 @@ private void verifyParameterDescriptors(Operation operation) { /** * Extracts the names of the parameters that were present in the given * {@code operation}. - * * @param operation the operation * @return the parameters */ @@ -145,47 +119,39 @@ private void verifyParameterDescriptors(Operation operation) { /** * Called when the documented parameters do not match the actual parameters. - * * @param undocumentedParameters the parameters that were found in the operation but * were not documented * @param missingParameters the parameters that were documented but were not found in * the operation */ - protected abstract void verificationFailed(Set undocumentedParameters, - Set missingParameters); + protected abstract void verificationFailed(Set undocumentedParameters, Set missingParameters); /** * Returns a {@code Map} of {@link ParameterDescriptor ParameterDescriptors} that will * be used to generate the documentation key by their * {@link ParameterDescriptor#getName()}. - * * @return the map of path descriptors - * @deprecated since 1.1.0 in favor of {@link #getParameterDescriptors()} */ - @Deprecated - protected final Map getFieldDescriptors() { + protected final Map getParameterDescriptors() { return this.descriptorsByName; } /** - * Returns a {@code Map} of {@link ParameterDescriptor ParameterDescriptors} that will - * be used to generate the documentation key by their - * {@link ParameterDescriptor#getName()}. - * - * @return the map of path descriptors + * Returns whether to ignore undocumented parameters. + * @return {@code true} if undocumented parameters should be ignored, otherwise + * {@code false} + * @since 2.0.5 */ - protected final Map getParameterDescriptors() { - return this.descriptorsByName; + protected final boolean isIgnoreUndocumentedParameters() { + return this.ignoreUndocumentedParameters; } /** * Returns a model for the given {@code descriptor}. - * * @param descriptor the descriptor * @return the model */ - protected Map createModelForDescriptor( - ParameterDescriptor descriptor) { + protected Map createModelForDescriptor(ParameterDescriptor descriptor) { Map model = new HashMap<>(); model.put("name", descriptor.getName()); model.put("description", descriptor.getDescription()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/FormParametersSnippet.java similarity index 60% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/request/FormParametersSnippet.java index 3a5ff2075..8d5401d51 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/FormParametersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,128 +22,115 @@ import java.util.Map; import java.util.Set; +import org.springframework.restdocs.operation.FormParameters; import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.SnippetException; /** - * A {@link Snippet} that documents the request parameters supported by a RESTful - * resource. - *

        - * Request parameters are sent as part of the query string or as POSTed form data. + * A {@link Snippet} that documents the form parameters supported by a RESTful resource. * * @author Andy Wilkinson - * @see OperationRequest#getParameters() - * @see RequestDocumentation#requestParameters(ParameterDescriptor...) - * @see RequestDocumentation#requestParameters(Map, ParameterDescriptor...) + * @since 3.0.0 + * @see RequestDocumentation#formParameters(ParameterDescriptor...) + * @see RequestDocumentation#formParameters(Map, ParameterDescriptor...) */ -public class RequestParametersSnippet extends AbstractParametersSnippet { +public class FormParametersSnippet extends AbstractParametersSnippet { /** - * Creates a new {@code RequestParametersSnippet} that will document the request's + * Creates a new {@code FormParametersSnippet} that will document the request's form * parameters using the given {@code descriptors}. Undocumented parameters will * trigger a failure. - * * @param descriptors the parameter descriptors */ - protected RequestParametersSnippet(List descriptors) { + protected FormParametersSnippet(List descriptors) { this(descriptors, null, false); } /** - * Creates a new {@code RequestParametersSnippet} that will document the request's + * Creates a new {@code FormParametersSnippet} that will document the request's form * parameters using the given {@code descriptors}. If * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will * be ignored and will not trigger a failure. - * * @param descriptors the parameter descriptors * @param ignoreUndocumentedParameters whether undocumented parameters should be * ignored */ - protected RequestParametersSnippet(List descriptors, - boolean ignoreUndocumentedParameters) { + protected FormParametersSnippet(List descriptors, boolean ignoreUndocumentedParameters) { this(descriptors, null, ignoreUndocumentedParameters); } /** - * Creates a new {@code RequestParametersSnippet} that will document the request's + * Creates a new {@code FormParametersSnippet} that will document the request's form * parameters using the given {@code descriptors}. The given {@code attributes} will * be included in the model during template rendering. Undocumented parameters will * trigger a failure. - * * @param descriptors the parameter descriptors * @param attributes the additional attributes */ - protected RequestParametersSnippet(List descriptors, - Map attributes) { + protected FormParametersSnippet(List descriptors, Map attributes) { this(descriptors, attributes, false); } /** - * Creates a new {@code RequestParametersSnippet} that will document the request's + * Creates a new {@code FormParametersSnippet} that will document the request's form * parameters using the given {@code descriptors}. The given {@code attributes} will * be included in the model during template rendering. If * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will * be ignored and will not trigger a failure. - * * @param descriptors the parameter descriptors * @param attributes the additional attributes * @param ignoreUndocumentedParameters whether undocumented parameters should be * ignored */ - protected RequestParametersSnippet(List descriptors, - Map attributes, boolean ignoreUndocumentedParameters) { - super("request-parameters", descriptors, attributes, - ignoreUndocumentedParameters); + protected FormParametersSnippet(List descriptors, Map attributes, + boolean ignoreUndocumentedParameters) { + super("form-parameters", descriptors, attributes, ignoreUndocumentedParameters); } @Override - protected void verificationFailed(Set undocumentedParameters, - Set missingParameters) { + protected void verificationFailed(Set undocumentedParameters, Set missingParameters) { String message = ""; if (!undocumentedParameters.isEmpty()) { - message += "Request parameters with the following names were not documented: " - + undocumentedParameters; + message += "Form parameters with the following names were not documented: " + undocumentedParameters; } if (!missingParameters.isEmpty()) { if (message.length() > 0) { message += ". "; } - message += "Request parameters with the following names were not found in the request: " - + missingParameters; + message += "Form parameters with the following names were not found in the request: " + missingParameters; } throw new SnippetException(message); } @Override protected Set extractActualParameters(Operation operation) { - return operation.getRequest().getParameters().keySet(); + return FormParameters.from(operation.getRequest()).keySet(); } /** - * Returns a new {@code RequestParametersSnippet} configured with this snippet's + * Returns a new {@code FormParametersSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public RequestParametersSnippet and(ParameterDescriptor... additionalDescriptors) { + public FormParametersSnippet and(ParameterDescriptor... additionalDescriptors) { return and(Arrays.asList(additionalDescriptors)); } /** - * Returns a new {@code RequestParametersSnippet} configured with this snippet's + * Returns a new {@code FormParametersSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public RequestParametersSnippet and(List additionalDescriptors) { - List combinedDescriptors = new ArrayList<>( - getParameterDescriptors().values()); + public FormParametersSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(getParameterDescriptors().values()); combinedDescriptors.addAll(additionalDescriptors); - return new RequestParametersSnippet(combinedDescriptors, this.getAttributes()); + return new FormParametersSnippet(combinedDescriptors, this.getAttributes(), + this.isIgnoreUndocumentedParameters()); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java index 82e633cd3..a3555211b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -34,7 +34,6 @@ public class ParameterDescriptor extends IgnorableDescriptor descriptors) { @@ -58,13 +57,11 @@ protected PathParametersSnippet(List descriptors) { * parameters using the given {@code descriptors}. If * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will * be ignored and will not trigger a failure. - * * @param descriptors the parameter descriptors * @param ignoreUndocumentedParameters whether undocumented parameters should be * ignored */ - protected PathParametersSnippet(List descriptors, - boolean ignoreUndocumentedParameters) { + protected PathParametersSnippet(List descriptors, boolean ignoreUndocumentedParameters) { this(descriptors, null, ignoreUndocumentedParameters); } @@ -73,12 +70,10 @@ protected PathParametersSnippet(List descriptors, * parameters using the given {@code descriptors}. The given {@code attributes} will * be included in the model during template rendering. Undocumented parameters will * trigger a failure. - * * @param descriptors the parameter descriptors * @param attributes the additional attributes */ - protected PathParametersSnippet(List descriptors, - Map attributes) { + protected PathParametersSnippet(List descriptors, Map attributes) { this(descriptors, attributes, false); } @@ -88,14 +83,13 @@ protected PathParametersSnippet(List descriptors, * be included in the model during template rendering. If * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will * be ignored and will not trigger a failure. - * * @param descriptors the parameter descriptors * @param attributes the additional attributes * @param ignoreUndocumentedParameters whether undocumented parameters should be * ignored */ - protected PathParametersSnippet(List descriptors, - Map attributes, boolean ignoreUndocumentedParameters) { + protected PathParametersSnippet(List descriptors, Map attributes, + boolean ignoreUndocumentedParameters) { super("path-parameters", descriptors, attributes, ignoreUndocumentedParameters); } @@ -128,7 +122,7 @@ protected Set extractActualParameters(Operation operation) { private String extractUrlTemplate(Operation operation) { String urlTemplate = (String) operation.getAttributes() - .get(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE); + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE); Assert.notNull(urlTemplate, "urlTemplate not found. If you are using MockMvc did " + "you use RestDocumentationRequestBuilders to build the request?"); return urlTemplate; @@ -136,23 +130,21 @@ private String extractUrlTemplate(Operation operation) { private static String getParameterName(String match) { int colonIndex = match.indexOf(':'); - return colonIndex != -1 ? match.substring(0, colonIndex) : match; + return (colonIndex != -1) ? match.substring(0, colonIndex) : match; } @Override - protected void verificationFailed(Set undocumentedParameters, - Set missingParameters) { + protected void verificationFailed(Set undocumentedParameters, Set missingParameters) { String message = ""; if (!undocumentedParameters.isEmpty()) { - message += "Path parameters with the following names were not documented: " - + undocumentedParameters; + message += "Path parameters with the following names were not documented: " + undocumentedParameters; } if (!missingParameters.isEmpty()) { if (message.length() > 0) { message += ". "; } - message += "Path parameters with the following names were not found in " - + "the request: " + missingParameters; + message += "Path parameters with the following names were not found in " + "the request: " + + missingParameters; } throw new SnippetException(message); } @@ -175,12 +167,10 @@ public final PathParametersSnippet and(ParameterDescriptor... additionalDescript * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public final PathParametersSnippet and( - List additionalDescriptors) { - List combinedDescriptors = new ArrayList<>( - getParameterDescriptors().values()); + public final PathParametersSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(getParameterDescriptors().values()); combinedDescriptors.addAll(additionalDescriptors); - return new PathParametersSnippet(combinedDescriptors, this.getAttributes()); + return new PathParametersSnippet(combinedDescriptors, this.getAttributes(), isIgnoreUndocumentedParameters()); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java new file mode 100644 index 000000000..2485c2f3d --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/QueryParametersSnippet.java @@ -0,0 +1,136 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package org.springframework.restdocs.request; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.QueryParameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; + +/** + * A {@link Snippet} that documents the query parameters supported by a RESTful resource. + * + * @author Andy Wilkinson + * @since 3.0.0 + * @see RequestDocumentation#queryParameters(ParameterDescriptor...) + * @see RequestDocumentation#queryParameters(Map, ParameterDescriptor...) + */ +public class QueryParametersSnippet extends AbstractParametersSnippet { + + /** + * Creates a new {@code QueryParametersSnippet} that will document the request's query + * parameters using the given {@code descriptors}. Undocumented parameters will + * trigger a failure. + * @param descriptors the parameter descriptors + */ + protected QueryParametersSnippet(List descriptors) { + this(descriptors, null, false); + } + + /** + * Creates a new {@code QueryParametersSnippet} that will document the request's query + * parameters using the given {@code descriptors}. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * @param descriptors the parameter descriptors + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected QueryParametersSnippet(List descriptors, boolean ignoreUndocumentedParameters) { + this(descriptors, null, ignoreUndocumentedParameters); + } + + /** + * Creates a new {@code QueryParametersSnippet} that will document the request's query + * parameters using the given {@code descriptors}. The given {@code attributes} will + * be included in the model during template rendering. Undocumented parameters will + * trigger a failure. + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + */ + protected QueryParametersSnippet(List descriptors, Map attributes) { + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code QueryParametersSnippet} that will document the request's query + * parameters using the given {@code descriptors}. The given {@code attributes} will + * be included in the model during template rendering. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected QueryParametersSnippet(List descriptors, Map attributes, + boolean ignoreUndocumentedParameters) { + super("query-parameters", descriptors, attributes, ignoreUndocumentedParameters); + } + + @Override + protected void verificationFailed(Set undocumentedParameters, Set missingParameters) { + String message = ""; + if (!undocumentedParameters.isEmpty()) { + message += "Query parameters with the following names were not documented: " + undocumentedParameters; + } + if (!missingParameters.isEmpty()) { + if (message.length() > 0) { + message += ". "; + } + message += "Query parameters with the following names were not found in the request: " + missingParameters; + } + throw new SnippetException(message); + } + + @Override + protected Set extractActualParameters(Operation operation) { + return QueryParameters.from(operation.getRequest()).keySet(); + } + + /** + * Returns a new {@code QueryParametersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public QueryParametersSnippet and(ParameterDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code QueryParametersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public QueryParametersSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(getParameterDescriptors().values()); + combinedDescriptors.addAll(additionalDescriptors); + return new QueryParametersSnippet(combinedDescriptors, this.getAttributes(), + this.isIgnoreUndocumentedParameters()); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 30228ac41..221006b83 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,8 +37,7 @@ private RequestDocumentation() { /** * Creates a {@link ParameterDescriptor} that describes a request or path parameter * with the given {@code name}. - * - * @param name The name of the parameter + * @param name the name of the parameter * @return a {@link ParameterDescriptor} ready for further configuration */ public static ParameterDescriptor parameterWithName(String name) { @@ -48,8 +47,7 @@ public static ParameterDescriptor parameterWithName(String name) { /** * Creates a {@link RequestPartDescriptor} that describes a request part with the * given {@code name}. - * - * @param name The name of the request part + * @param name the name of the request part * @return a {@link RequestPartDescriptor} ready for further configuration */ public static RequestPartDescriptor partWithName(String name) { @@ -67,14 +65,12 @@ public static RequestPartDescriptor partWithName(String name) { * request path, a failure will also occur. *

        * If you do not want to document a path parameter, a parameter descriptor can be - * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing - * in the generated snippet while avoiding the failure described above. - * + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters */ - public static PathParametersSnippet pathParameters( - ParameterDescriptor... descriptors) { + public static PathParametersSnippet pathParameters(ParameterDescriptor... descriptors) { return pathParameters(Arrays.asList(descriptors)); } @@ -89,14 +85,12 @@ public static PathParametersSnippet pathParameters( * request path, a failure will also occur. *

        * If you do not want to document a path parameter, a parameter descriptor can be - * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing - * in the generated snippet while avoiding the failure described above. - * + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters */ - public static PathParametersSnippet pathParameters( - List descriptors) { + public static PathParametersSnippet pathParameters(List descriptors) { return new PathParametersSnippet(descriptors); } @@ -107,12 +101,10 @@ public static PathParametersSnippet pathParameters( *

        * If a parameter is documented, is not marked as optional, and is not present in the * response, a failure will occur. Any undocumented parameters will be ignored. - * * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters */ - public static PathParametersSnippet relaxedPathParameters( - ParameterDescriptor... descriptors) { + public static PathParametersSnippet relaxedPathParameters(ParameterDescriptor... descriptors) { return relaxedPathParameters(Arrays.asList(descriptors)); } @@ -123,12 +115,10 @@ public static PathParametersSnippet relaxedPathParameters( *

        * If a parameter is documented, is not marked as optional, and is not present in the * response, a failure will occur. Any undocumented parameters will be ignored. - * * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters */ - public static PathParametersSnippet relaxedPathParameters( - List descriptors) { + public static PathParametersSnippet relaxedPathParameters(List descriptors) { return new PathParametersSnippet(descriptors, true); } @@ -144,9 +134,8 @@ public static PathParametersSnippet relaxedPathParameters( * request path, a failure will also occur. *

        * If you do not want to document a path parameter, a parameter descriptor can be - * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing - * in the generated snippet while avoiding the failure described above. - * + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. * @param attributes the attributes * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters @@ -168,9 +157,8 @@ public static PathParametersSnippet pathParameters(Map attribute * request path, a failure will also occur. *

        * If you do not want to document a path parameter, a parameter descriptor can be - * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing - * in the generated snippet while avoiding the failure described above. - * + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. * @param attributes the attributes * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters @@ -188,13 +176,12 @@ public static PathParametersSnippet pathParameters(Map attribute *

        * If a parameter is documented, is not marked as optional, and is not present in the * response, a failure will occur. Any undocumented parameters will be ignored. - * * @param attributes the attributes * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters */ - public static PathParametersSnippet relaxedPathParameters( - Map attributes, ParameterDescriptor... descriptors) { + public static PathParametersSnippet relaxedPathParameters(Map attributes, + ParameterDescriptor... descriptors) { return relaxedPathParameters(attributes, Arrays.asList(descriptors)); } @@ -206,182 +193,331 @@ public static PathParametersSnippet relaxedPathParameters( *

        * If a parameter is documented, is not marked as optional, and is not present in the * response, a failure will occur. Any undocumented parameters will be ignored. - * * @param attributes the attributes * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters */ - public static PathParametersSnippet relaxedPathParameters( - Map attributes, List descriptors) { + public static PathParametersSnippet relaxedPathParameters(Map attributes, + List descriptors) { return new PathParametersSnippet(descriptors, attributes, true); } /** - * Returns a {@code Snippet} that will document the parameters from the API - * operation's request. The parameters will be documented using the given + * Returns a {@code Snippet} that will document the query parameters from the API + * operation's request. The query parameters will be documented using the given * {@code descriptors}. *

        - * If a parameter is present in the request, but is not documented by one of the + * If a query parameter is present in the request, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * parameter is documented, is not marked as optional, and is not present in the + * query parameter is documented, is not marked as optional, and is not present in the * request, a failure will also occur. *

        - * If you do not want to document a request parameter, a parameter descriptor can be - * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing - * in the generated snippet while avoiding the failure described above. - * - * @param descriptors The descriptions of the request's parameters + * If you do not want to document a query parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. + * @param descriptors the descriptions of the request's query parameters * @return the snippet - * @see OperationRequest#getParameters() + * @since 3.0.0 */ - public static RequestParametersSnippet requestParameters( - ParameterDescriptor... descriptors) { - return requestParameters(Arrays.asList(descriptors)); + public static QueryParametersSnippet queryParameters(ParameterDescriptor... descriptors) { + return queryParameters(Arrays.asList(descriptors)); } /** - * Returns a {@code Snippet} that will document the parameters from the API - * operation's request. The parameters will be documented using the given + * Returns a {@code Snippet} that will document the query parameters from the API + * operation's request. The query parameters will be documented using the given * {@code descriptors}. *

        - * If a parameter is present in the request, but is not documented by one of the + * If a query parameter is present in the request, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * parameter is documented, is not marked as optional, and is not present in the + * query parameter is documented, is not marked as optional, and is not present in the * request, a failure will also occur. *

        - * If you do not want to document a request parameter, a parameter descriptor can be - * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing - * in the generated snippet while avoiding the failure described above. - * - * @param descriptors The descriptions of the request's parameters + * If you do not want to document a query parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. + * @param descriptors the descriptions of the request's query parameters * @return the snippet - * @see OperationRequest#getParameters() + * @since 3.0.0 */ - public static RequestParametersSnippet requestParameters( - List descriptors) { - return new RequestParametersSnippet(descriptors); + public static QueryParametersSnippet queryParameters(List descriptors) { + return new QueryParametersSnippet(descriptors); } /** - * Returns a {@code Snippet} that will document the parameters from the API - * operation's request. The parameters will be documented using the given + * Returns a {@code Snippet} that will document the query parameters from the API + * operation's request. The query parameters will be documented using the given + * {@code descriptors}. + *

        + * If a query parameter is documented, is not marked as optional, and is not present + * in the response, a failure will occur. Any undocumented query parameters will be + * ignored. + * @param descriptors the descriptions of the request's query parameters + * @return the snippet + * @since 3.0.0 + */ + public static QueryParametersSnippet relaxedQueryParameters(ParameterDescriptor... descriptors) { + return relaxedQueryParameters(Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the query parameters from the API + * operation's request. The query parameters will be documented using the given * {@code descriptors}. *

        * If a parameter is documented, is not marked as optional, and is not present in the - * response, a failure will occur. Any undocumented parameters will be ignored. - * - * @param descriptors The descriptions of the request's parameters + * response, a failure will occur. Any undocumented query parameters will be ignored. + * @param descriptors the descriptions of the request's query parameters * @return the snippet - * @see OperationRequest#getParameters() + * @since 3.0.0 */ - public static RequestParametersSnippet relaxedRequestParameters( + public static QueryParametersSnippet relaxedQueryParameters(List descriptors) { + return new QueryParametersSnippet(descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the query parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the query parameters will be documented using the given + * {@code descriptors} . + *

        + * If a query parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * query parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. + *

        + * If you do not want to document a query parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. + * @param attributes the attributes + * @param descriptors the descriptions of the request's query parameters + * @return the snippet that will document the query parameters + * @since 3.0.0 + */ + public static QueryParametersSnippet queryParameters(Map attributes, ParameterDescriptor... descriptors) { - return relaxedRequestParameters(Arrays.asList(descriptors)); + return queryParameters(attributes, Arrays.asList(descriptors)); } /** - * Returns a {@code Snippet} that will document the parameters from the API - * operation's request. The parameters will be documented using the given + * Returns a {@code Snippet} that will document the query parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the query parameters will be documented using the given + * {@code descriptors} . + *

        + * If a query parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * query parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. + *

        + * If you do not want to document a query parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. + * @param attributes the attributes + * @param descriptors the descriptions of the request's query parameters + * @return the snippet that will document the query parameters + * @since 3.0.0 + */ + public static QueryParametersSnippet queryParameters(Map attributes, + List descriptors) { + return new QueryParametersSnippet(descriptors, attributes); + } + + /** + * Returns a {@code Snippet} that will document the query parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the query parameters will be documented using the given + * {@code descriptors} . + *

        + * If a query parameter is documented, is not marked as optional, and is not present + * in the response, a failure will occur. Any undocumented query parameters will be + * ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the request's query parameters + * @return the snippet that will document the query parameters + * @since 3.0.0 + */ + public static QueryParametersSnippet relaxedQueryParameters(Map attributes, + ParameterDescriptor... descriptors) { + return relaxedQueryParameters(attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the query parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the query parameters will be documented using the given + * {@code descriptors} . + *

        + * If a query parameter is documented, is not marked as optional, and is not present + * in the response, a failure will occur. Any undocumented query parameters will be + * ignored. + * @param attributes the attributes + * @param descriptors the descriptions of the request's query parameters + * @return the snippet that will document the query parameters + * @since 3.0.0 + */ + public static QueryParametersSnippet relaxedQueryParameters(Map attributes, + List descriptors) { + return new QueryParametersSnippet(descriptors, attributes, true); + } + + /** + * Returns a {@code Snippet} that will document the form parameters from the API + * operation's request. The form parameters will be documented using the given + * {@code descriptors}. + *

        + * If a form parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a form + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. + *

        + * If you do not want to document a form parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. + * @param descriptors the descriptions of the request's form parameters + * @return the snippet + * @since 3.0.0 + */ + public static FormParametersSnippet formParameters(ParameterDescriptor... descriptors) { + return formParameters(Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the form parameters from the API + * operation's request. The form parameters will be documented using the given + * {@code descriptors}. + *

        + * If a form parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a form + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. + *

        + * If you do not want to document a form parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. + * @param descriptors the descriptions of the request's form parameters + * @return the snippet + * @since 3.0.0 + */ + public static FormParametersSnippet formParameters(List descriptors) { + return new FormParametersSnippet(descriptors); + } + + /** + * Returns a {@code Snippet} that will document the form parameters from the API + * operation's request. The form parameters will be documented using the given + * {@code descriptors}. + *

        + * If a form parameter is documented, is not marked as optional, and is not present in + * the response, a failure will occur. Any undocumented form parameters will be + * ignored. + * @param descriptors the descriptions of the request's form parameters + * @return the snippet + * @since 3.0.0 + */ + public static FormParametersSnippet relaxedFormParameters(ParameterDescriptor... descriptors) { + return relaxedFormParameters(Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the form parameters from the API + * operation's request. The form parameters will be documented using the given * {@code descriptors}. *

        * If a parameter is documented, is not marked as optional, and is not present in the - * response, a failure will occur. Any undocumented parameters will be ignored. - * - * @param descriptors The descriptions of the request's parameters + * response, a failure will occur. Any undocumented form parameters will be ignored. + * @param descriptors the descriptions of the request's form parameters * @return the snippet - * @see OperationRequest#getParameters() + * @since 3.0.0 */ - public static RequestParametersSnippet relaxedRequestParameters( - List descriptors) { - return new RequestParametersSnippet(descriptors, true); + public static FormParametersSnippet relaxedFormParameters(List descriptors) { + return new FormParametersSnippet(descriptors, true); } /** - * Returns a {@code Snippet} that will document the parameters from the API + * Returns a {@code Snippet} that will document the form parameters from the API * operation's request. The given {@code attributes} will be available during snippet - * rendering and the parameters will be documented using the given {@code descriptors} - * . + * rendering and the form parameters will be documented using the given + * {@code descriptors} . *

        - * If a parameter is present in the request, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * If a form parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a form * parameter is documented, is not marked as optional, and is not present in the * request, a failure will also occur. *

        - * If you do not want to document a request parameter, a parameter descriptor can be - * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing - * in the generated snippet while avoiding the failure described above. - * + * If you do not want to document a form parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. * @param attributes the attributes - * @param descriptors the descriptions of the request's parameters - * @return the snippet that will document the parameters - * @see OperationRequest#getParameters() + * @param descriptors the descriptions of the request's form parameters + * @return the snippet that will document the form parameters + * @since 3.0.0 */ - public static RequestParametersSnippet requestParameters( - Map attributes, ParameterDescriptor... descriptors) { - return requestParameters(attributes, Arrays.asList(descriptors)); + public static FormParametersSnippet formParameters(Map attributes, + ParameterDescriptor... descriptors) { + return formParameters(attributes, Arrays.asList(descriptors)); } /** - * Returns a {@code Snippet} that will document the parameters from the API + * Returns a {@code Snippet} that will document the form parameters from the API * operation's request. The given {@code attributes} will be available during snippet - * rendering and the parameters will be documented using the given {@code descriptors} - * . + * rendering and the form parameters will be documented using the given + * {@code descriptors} . *

        - * If a parameter is present in the request, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * If a form parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a form * parameter is documented, is not marked as optional, and is not present in the * request, a failure will also occur. *

        - * If you do not want to document a request parameter, a parameter descriptor can be - * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing - * in the generated snippet while avoiding the failure described above. - * + * If you do not want to document a form parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored()}. This will prevent it from + * appearing in the generated snippet while avoiding the failure described above. * @param attributes the attributes - * @param descriptors the descriptions of the request's parameters - * @return the snippet that will document the parameters - * @see OperationRequest#getParameters() + * @param descriptors the descriptions of the request's form parameters + * @return the snippet that will document the form parameters + * @since 3.0.0 */ - public static RequestParametersSnippet requestParameters( - Map attributes, List descriptors) { - return new RequestParametersSnippet(descriptors, attributes); + public static FormParametersSnippet formParameters(Map attributes, + List descriptors) { + return new FormParametersSnippet(descriptors, attributes); } /** - * Returns a {@code Snippet} that will document the parameters from the API + * Returns a {@code Snippet} that will document the form parameters from the API * operation's request. The given {@code attributes} will be available during snippet - * rendering and the parameters will be documented using the given {@code descriptors} - * . + * rendering and the form parameters will be documented using the given + * {@code descriptors} . *

        - * If a parameter is documented, is not marked as optional, and is not present in the - * response, a failure will occur. Any undocumented parameters will be ignored. - * + * If a form parameter is documented, is not marked as optional, and is not present in + * the response, a failure will occur. Any undocumented form parameters will be + * ignored. * @param attributes the attributes - * @param descriptors the descriptions of the request's parameters - * @return the snippet that will document the parameters - * @see OperationRequest#getParameters() + * @param descriptors the descriptions of the request's form parameters + * @return the snippet that will document the form parameters + * @since 3.0.0 */ - public static RequestParametersSnippet relaxedRequestParameters( - Map attributes, ParameterDescriptor... descriptors) { - return relaxedRequestParameters(attributes, Arrays.asList(descriptors)); + public static FormParametersSnippet relaxedFormParameters(Map attributes, + ParameterDescriptor... descriptors) { + return relaxedFormParameters(attributes, Arrays.asList(descriptors)); } /** - * Returns a {@code Snippet} that will document the parameters from the API + * Returns a {@code Snippet} that will document the form parameters from the API * operation's request. The given {@code attributes} will be available during snippet - * rendering and the parameters will be documented using the given {@code descriptors} - * . + * rendering and the form parameters will be documented using the given + * {@code descriptors} . *

        - * If a parameter is documented, is not marked as optional, and is not present in the - * response, a failure will occur. Any undocumented parameters will be ignored. - * + * If a form parameter is documented, is not marked as optional, and is not present in + * the response, a failure will occur. Any undocumented form parameters will be + * ignored. * @param attributes the attributes - * @param descriptors the descriptions of the request's parameters - * @return the snippet that will document the parameters - * @see OperationRequest#getParameters() + * @param descriptors the descriptions of the request's form parameters + * @return the snippet that will document the form parameters + * @since 3.0.0 */ - public static RequestParametersSnippet relaxedRequestParameters( - Map attributes, List descriptors) { - return new RequestParametersSnippet(descriptors, attributes, true); + public static FormParametersSnippet relaxedFormParameters(Map attributes, + List descriptors) { + return new FormParametersSnippet(descriptors, attributes, true); } /** @@ -394,10 +530,9 @@ public static RequestParametersSnippet relaxedRequestParameters( * failure will also occur. *

        * If you do not want to document a part, a part descriptor can be marked as - * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the + * {@link RequestPartDescriptor#ignored()}. This will prevent it from appearing in the * generated snippet while avoiding the failure described above. - * - * @param descriptors The descriptions of the request's parts + * @param descriptors the descriptions of the request's parts * @return the snippet * @see OperationRequest#getParts() */ @@ -415,15 +550,13 @@ public static RequestPartsSnippet requestParts(RequestPartDescriptor... descript * failure will also occur. *

        * If you do not want to document a part, a part descriptor can be marked as - * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the + * {@link RequestPartDescriptor#ignored()}. This will prevent it from appearing in the * generated snippet while avoiding the failure described above. - * - * @param descriptors The descriptions of the request's parts + * @param descriptors the descriptions of the request's parts * @return the snippet * @see OperationRequest#getParts() */ - public static RequestPartsSnippet requestParts( - List descriptors) { + public static RequestPartsSnippet requestParts(List descriptors) { return new RequestPartsSnippet(descriptors); } @@ -433,13 +566,11 @@ public static RequestPartsSnippet requestParts( *

        * If a part is documented, is not marked as optional, and is not present in the * request, a failure will occur. Any undocumented parts will be ignored. - * - * @param descriptors The descriptions of the request's parts + * @param descriptors the descriptions of the request's parts * @return the snippet * @see OperationRequest#getParts() */ - public static RequestPartsSnippet relaxedRequestParts( - RequestPartDescriptor... descriptors) { + public static RequestPartsSnippet relaxedRequestParts(RequestPartDescriptor... descriptors) { return relaxedRequestParts(Arrays.asList(descriptors)); } @@ -449,13 +580,11 @@ public static RequestPartsSnippet relaxedRequestParts( *

        * If a part is documented, is not marked as optional, and is not present in the * request, a failure will occur. Any undocumented parts will be ignored. - * - * @param descriptors The descriptions of the request's parts + * @param descriptors the descriptions of the request's parts * @return the snippet * @see OperationRequest#getParts() */ - public static RequestPartsSnippet relaxedRequestParts( - List descriptors) { + public static RequestPartsSnippet relaxedRequestParts(List descriptors) { return new RequestPartsSnippet(descriptors, true); } @@ -470,9 +599,8 @@ public static RequestPartsSnippet relaxedRequestParts( * failure will also occur. *

        * If you do not want to document a part, a part descriptor can be marked as - * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the + * {@link RequestPartDescriptor#ignored()}. This will prevent it from appearing in the * generated snippet while avoiding the failure described above. - * * @param attributes the attributes * @param descriptors the descriptions of the request's parts * @return the snippet @@ -494,9 +622,8 @@ public static RequestPartsSnippet requestParts(Map attributes, * failure will also occur. *

        * If you do not want to document a part, a part descriptor can be marked as - * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the + * {@link RequestPartDescriptor#ignored()}. This will prevent it from appearing in the * generated snippet while avoiding the failure described above. - * * @param attributes the attributes * @param descriptors the descriptions of the request's parts * @return the snippet @@ -514,11 +641,9 @@ public static RequestPartsSnippet requestParts(Map attributes, *

        * If a part is documented, is not marked as optional, and is not present in the * request, a failure will occur. Any undocumented parts will be ignored. - * * @param attributes the attributes * @param descriptors the descriptions of the request's parts * @return the snippet - * @see OperationRequest#getParameters() */ public static RequestPartsSnippet relaxedRequestParts(Map attributes, RequestPartDescriptor... descriptors) { @@ -532,11 +657,9 @@ public static RequestPartsSnippet relaxedRequestParts(Map attrib *

        * If a part is documented, is not marked as optional, and is not present in the * request, a failure will occur. Any undocumented parts will be ignored. - * * @param attributes the attributes * @param descriptors the descriptions of the request's parts * @return the snippet - * @see OperationRequest#getParameters() */ public static RequestPartsSnippet relaxedRequestParts(Map attributes, List descriptors) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java index bbbc322ef..fe1f83ec9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java @@ -34,7 +34,6 @@ public class RequestPartDescriptor extends IgnorableDescriptor descriptors) { @@ -64,12 +63,10 @@ protected RequestPartsSnippet(List descriptors) { * Creates a new {@code RequestPartsSnippet} that will document the request's parts * using the given {@code descriptors}. If {@code ignoreUndocumentedParts} is * {@code true}, undocumented parts will be ignored and will not trigger a failure. - * * @param descriptors the parameter descriptors * @param ignoreUndocumentedParts whether undocumented parts should be ignored */ - protected RequestPartsSnippet(List descriptors, - boolean ignoreUndocumentedParts) { + protected RequestPartsSnippet(List descriptors, boolean ignoreUndocumentedParts) { this(descriptors, null, ignoreUndocumentedParts); } @@ -77,12 +74,10 @@ protected RequestPartsSnippet(List descriptors, * Creates a new {@code RequestPartsSnippet} that will document the request's parts * using the given {@code descriptors}. The given {@code attributes} will be included * in the model during template rendering. Undocumented parts will trigger a failure. - * * @param descriptors the parameter descriptors * @param attributes the additional attributes */ - protected RequestPartsSnippet(List descriptors, - Map attributes) { + protected RequestPartsSnippet(List descriptors, Map attributes) { this(descriptors, attributes, false); } @@ -91,22 +86,18 @@ protected RequestPartsSnippet(List descriptors, * using the given {@code descriptors}. The given {@code attributes} will be included * in the model during template rendering. If {@code ignoreUndocumentedParts} is * {@code true}, undocumented parts will be ignored and will not trigger a failure. - * * @param descriptors the parameter descriptors * @param attributes the additional attributes * @param ignoreUndocumentedParts whether undocumented parts should be ignored */ - protected RequestPartsSnippet(List descriptors, - Map attributes, boolean ignoreUndocumentedParts) { + protected RequestPartsSnippet(List descriptors, Map attributes, + boolean ignoreUndocumentedParts) { super("request-parts", attributes); for (RequestPartDescriptor descriptor : descriptors) { - Assert.notNull(descriptor.getName(), - "Request part descriptors must have a name"); + Assert.notNull(descriptor.getName(), "Request part descriptors must have a name"); if (!descriptor.isIgnored()) { - Assert.notNull(descriptor.getDescription(), - "The descriptor for request part '" + descriptor.getName() - + "' must either have a description or be marked as " - + "ignored"); + Assert.notNull(descriptor.getDescription(), "The descriptor for request part '" + descriptor.getName() + + "' must either have a description or be marked as " + "ignored"); } this.descriptorsByName.put(descriptor.getName(), descriptor); } @@ -129,10 +120,8 @@ public final RequestPartsSnippet and(RequestPartDescriptor... additionalDescript * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public final RequestPartsSnippet and( - List additionalDescriptors) { - List combinedDescriptors = new ArrayList<>( - this.descriptorsByName.values()); + public final RequestPartsSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(this.descriptorsByName.values()); combinedDescriptors.addAll(additionalDescriptors); return new RequestPartsSnippet(combinedDescriptors, this.getAttributes()); } @@ -142,8 +131,7 @@ protected Map createModel(Operation operation) { verifyRequestPartDescriptors(operation); Map model = new HashMap<>(); List> requestParts = new ArrayList<>(); - for (Entry entry : this.descriptorsByName - .entrySet()) { + for (Entry entry : this.descriptorsByName.entrySet()) { RequestPartDescriptor descriptor = entry.getValue(); if (!descriptor.isIgnored()) { requestParts.add(createModelForDescriptor(descriptor)); @@ -156,8 +144,7 @@ protected Map createModel(Operation operation) { private void verifyRequestPartDescriptors(Operation operation) { Set actualRequestParts = extractActualRequestParts(operation); Set expectedRequestParts = new HashSet<>(); - for (Entry entry : this.descriptorsByName - .entrySet()) { + for (Entry entry : this.descriptorsByName.entrySet()) { if (!entry.getValue().isOptional()) { expectedRequestParts.add(entry.getKey()); } @@ -187,25 +174,22 @@ private Set extractActualRequestParts(Operation operation) { return actualRequestParts; } - private void verificationFailed(Set undocumentedRequestParts, - Set missingRequestParts) { + private void verificationFailed(Set undocumentedRequestParts, Set missingRequestParts) { String message = ""; if (!undocumentedRequestParts.isEmpty()) { - message += "Request parts with the following names were not documented: " - + undocumentedRequestParts; + message += "Request parts with the following names were not documented: " + undocumentedRequestParts; } if (!missingRequestParts.isEmpty()) { if (message.length() > 0) { message += ". "; } - message += "Request parts with the following names were not found in " - + "the request: " + missingRequestParts; + message += "Request parts with the following names were not found in " + "the request: " + + missingRequestParts; } throw new SnippetException(message); } - private Map createModelForDescriptor( - RequestPartDescriptor descriptor) { + private Map createModelForDescriptor(RequestPartDescriptor descriptor) { Map model = new HashMap<>(); model.put("name", descriptor.getName()); model.put("description", descriptor.getDescription()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java index 0df7da34b..0df6ecbc1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/AbstractDescriptor.java @@ -36,7 +36,6 @@ public abstract class AbstractDescriptor> { /** * Adds the given {@code attributes} to the descriptor. - * * @param attributes the attributes * @return the descriptor */ @@ -50,7 +49,6 @@ public final T attributes(Attribute... attributes) { /** * Specifies the description. - * * @param description the description * @return the descriptor */ @@ -62,7 +60,6 @@ public final T description(Object description) { /** * Returns the description. - * * @return the description */ public final Object getDescription() { @@ -71,7 +68,6 @@ public final Object getDescription() { /** * Returns the descriptor's attributes. - * * @return the attributes */ public final Map getAttributes() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Attributes.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Attributes.java index 7506dd13c..d49131638 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Attributes.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Attributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,9 +33,8 @@ private Attributes() { /** * Creates an attribute with the given {@code key}. A value for the attribute must * still be specified. - * - * @param key The key of the attribute - * @return An {@code AttributeBuilder} to use to specify the value of the attribute + * @param key the key of the attribute + * @return an {@code AttributeBuilder} to use to specify the value of the attribute * @see AttributeBuilder#value(Object) */ public static AttributeBuilder key(String key) { @@ -44,9 +43,8 @@ public static AttributeBuilder key(String key) { /** * Creates a {@code Map} of the given {@code attributes}. - * - * @param attributes The attributes - * @return A Map of the attributes + * @param attributes the attributes + * @return a Map of the attributes */ public static Map attributes(Attribute... attributes) { Map attributeMap = new HashMap<>(); @@ -69,9 +67,8 @@ private AttributeBuilder(String key) { /** * Configures the value of the attribute. - * - * @param value The attribute's value - * @return A newly created {@code Attribute} + * @param value the attribute's value + * @return a newly created {@code Attribute} */ public Attribute value(Object value) { return new Attribute(this.key, value); @@ -90,7 +87,6 @@ public static final class Attribute { /** * Creates a new attribute with the given {@code key} and {@code value}. - * * @param key the key * @param value the value */ @@ -101,7 +97,6 @@ public Attribute(String key, Object value) { /** * Returns the attribute's key. - * * @return the key */ public String getKey() { @@ -110,7 +105,6 @@ public String getKey() { /** * Returns the attribute's value. - * * @return the value */ public Object getValue() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java index 1031d811c..d3ba77250 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,15 +22,13 @@ * @param the type of the descriptor * @author Andy Wilkinson */ -public abstract class IgnorableDescriptor> - extends AbstractDescriptor { +public abstract class IgnorableDescriptor> extends AbstractDescriptor { private boolean ignored = false; /** * Marks the described item as being ignored. Ignored items are not included in the * generated documentation. - * * @return the descriptor */ @SuppressWarnings("unchecked") @@ -42,7 +40,6 @@ public final T ignored() { /** * Returns whether or not the item being described should be ignored and, therefore, * should not be included in the documentation. - * * @return {@code true} if the item should be ignored, otherwise {@code false}. */ public final boolean isIgnored() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java index e83b22074..8ae827247 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/ModelCreationException.java @@ -28,7 +28,6 @@ public class ModelCreationException extends RuntimeException { /** * Creates a new {@code ModelCreationException} with the given {@code cause}. - * * @param cause the cause */ public ModelCreationException(Throwable cause) { @@ -38,7 +37,6 @@ public ModelCreationException(Throwable cause) { /** * Creates a new {@code ModelCreationException} with the given {@code message} and * {@code cause}. - * * @param message the message * @param cause the cause */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java index 5aff77981..4f51238bb 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java @@ -29,7 +29,6 @@ public interface PlaceholderResolverFactory { /** * Creates a new {@link PlaceholderResolver} using the given {@code context}. - * * @param context the context * @return the placeholder resolver */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java index 0fde0787d..df8a743b1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.restdocs.snippet; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -51,14 +48,11 @@ */ public class RestDocumentationContextPlaceholderResolver implements PlaceholderResolver { - private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([A-Z])"); - private final RestDocumentationContext context; /** * Creates a new placeholder resolver that will resolve placeholders using the given * {@code context}. - * * @param context the context to use */ public RestDocumentationContextPlaceholderResolver(RestDocumentationContext context) { @@ -105,7 +99,6 @@ private String tryClassNameConversion(String placeholderName) { /** * Converts the given {@code string} from camelCase to kebab-case. - * * @param string the string * @return the converted string */ @@ -115,7 +108,6 @@ protected final String camelCaseToKebabCase(String string) { /** * Converts the given {@code string} from camelCase to snake_case. - * * @param string the string * @return the converted string */ @@ -126,7 +118,6 @@ protected final String camelCaseToSnakeCase(String string) { /** * Returns the {@link RestDocumentationContext} that should be used during placeholder * resolution. - * * @return the context */ protected final RestDocumentationContext getContext() { @@ -134,15 +125,18 @@ protected final RestDocumentationContext getContext() { } private String camelCaseToSeparator(String string, String separator) { - Matcher matcher = CAMEL_CASE_PATTERN.matcher(string); StringBuffer result = new StringBuffer(); - while (matcher.find()) { - String replacement = (matcher.start() > 0) - ? separator + matcher.group(1).toLowerCase() - : matcher.group(1).toLowerCase(); - matcher.appendReplacement(result, replacement); + char[] chars = string.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char current = chars[i]; + if (Character.isUpperCase(current) && i > 0) { + if (Character.isLowerCase(chars[i - 1]) + || (i < chars.length - 1 && Character.isLowerCase(chars[i + 1]))) { + result.append(separator); + } + } + result.append(Character.toLowerCase(chars[i])); } - matcher.appendTail(result); return result.toString(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java index df8e075cb..852b9b8d0 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,8 +26,7 @@ * @author Andy Wilkinson * @since 1.1.0 */ -public final class RestDocumentationContextPlaceholderResolverFactory - implements PlaceholderResolverFactory { +public final class RestDocumentationContextPlaceholderResolverFactory implements PlaceholderResolverFactory { @Override public PlaceholderResolver create(RestDocumentationContext context) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Snippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Snippet.java index 77fa39004..c4c4294cc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Snippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/Snippet.java @@ -29,7 +29,6 @@ public interface Snippet { /** * Documents the call to the RESTful API described by the given {@code operation}. - * * @param operation the API operation * @throws IOException if a failure occurs will documenting the operation */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetException.java index e7a937443..909acf175 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetException.java @@ -27,7 +27,6 @@ public class SnippetException extends RuntimeException { /** * Creates a new {@code SnippetException} described by the given {@code message}. - * * @param message the message that describes the problem */ public SnippetException(String message) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index ab2ea99cd..2310be302 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -37,29 +36,12 @@ public final class StandardWriterResolver implements WriterResolver { private final PlaceholderResolverFactory placeholderResolverFactory; - private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper( - "{", "}"); + private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("{", "}"); private String encoding = "UTF-8"; private TemplateFormat templateFormat; - /** - * Creates a new {@code StandardWriterResolver} that will use the given - * {@code placeholderResolver} to resolve any placeholders in the - * {@code operationName}. Writers will use {@code UTF-8} encoding and, when writing to - * a file, will use a filename appropriate for Asciidoctor content. - * - * @param placeholderResolver the placeholder resolver - * @deprecated since 1.1.0 in favor of - * {@link #StandardWriterResolver(PlaceholderResolverFactory, String, TemplateFormat)} - */ - @Deprecated - public StandardWriterResolver(PlaceholderResolver placeholderResolver) { - this(new SingleInstancePlaceholderResolverFactory(placeholderResolver), "UTF-8", - TemplateFormats.asciidoctor()); - } - /** * Creates a new {@code StandardWriterResolver} that will use a * {@link PlaceholderResolver} created from the given @@ -67,44 +49,39 @@ public StandardWriterResolver(PlaceholderResolver placeholderResolver) { * {@code operationName}. Writers will use the given {@code encoding} and, when * writing to a file, will use a filename appropriate for content generated from * templates in the given {@code templateFormat}. - * * @param placeholderResolverFactory the placeholder resolver factory * @param encoding the encoding * @param templateFormat the snippet format */ - public StandardWriterResolver(PlaceholderResolverFactory placeholderResolverFactory, - String encoding, TemplateFormat templateFormat) { + public StandardWriterResolver(PlaceholderResolverFactory placeholderResolverFactory, String encoding, + TemplateFormat templateFormat) { this.placeholderResolverFactory = placeholderResolverFactory; this.encoding = encoding; this.templateFormat = templateFormat; } @Override - public Writer resolve(String operationName, String snippetName, - RestDocumentationContext context) throws IOException { - File outputFile = resolveFile( - this.propertyPlaceholderHelper.replacePlaceholders(operationName, - this.placeholderResolverFactory.create(context)), - snippetName + "." + this.templateFormat.getFileExtension(), context); - + public Writer resolve(String operationName, String snippetName, RestDocumentationContext context) + throws IOException { + PlaceholderResolver placeholderResolver = this.placeholderResolverFactory.create(context); + String outputDirectory = replacePlaceholders(placeholderResolver, operationName); + String fileName = replacePlaceholders(placeholderResolver, snippetName) + "." + + this.templateFormat.getFileExtension(); + File outputFile = resolveFile(outputDirectory, fileName, context); if (outputFile != null) { createDirectoriesIfNecessary(outputFile); - return new OutputStreamWriter(new FileOutputStream(outputFile), - this.encoding); + return new OutputStreamWriter(new FileOutputStream(outputFile), this.encoding); } else { return new OutputStreamWriter(System.out, this.encoding); } } - @Override - @Deprecated - public void setEncoding(String encoding) { - this.encoding = encoding; + private String replacePlaceholders(PlaceholderResolver resolver, String input) { + return this.propertyPlaceholderHelper.replacePlaceholders(input, resolver); } - File resolveFile(String outputDirectory, String fileName, - RestDocumentationContext context) { + File resolveFile(String outputDirectory, String fileName, RestDocumentationContext context) { File outputFile = new File(outputDirectory, fileName); if (!outputFile.isAbsolute()) { outputFile = makeRelativeToConfiguredOutputDir(outputFile, context); @@ -112,8 +89,7 @@ File resolveFile(String outputDirectory, String fileName, return outputFile; } - private File makeRelativeToConfiguredOutputDir(File outputFile, - RestDocumentationContext context) { + private File makeRelativeToConfiguredOutputDir(File outputFile, RestDocumentationContext context) { File configuredOutputDir = context.getOutputDirectory(); if (configuredOutputDir != null) { return new File(configuredOutputDir, outputFile.getPath()); @@ -124,26 +100,8 @@ private File makeRelativeToConfiguredOutputDir(File outputFile, private void createDirectoriesIfNecessary(File outputFile) { File parent = outputFile.getParentFile(); if (!parent.isDirectory() && !parent.mkdirs()) { - throw new IllegalStateException( - "Failed to create directory '" + parent + "'"); + throw new IllegalStateException("Failed to create directory '" + parent + "'"); } } - private static final class SingleInstancePlaceholderResolverFactory - implements PlaceholderResolverFactory { - - private final PlaceholderResolver placeholderResolver; - - private SingleInstancePlaceholderResolverFactory( - PlaceholderResolver placeholderResolver) { - this.placeholderResolver = placeholderResolver; - } - - @Override - public PlaceholderResolver create(RestDocumentationContext context) { - return this.placeholderResolver; - } - - } - } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index 2803cebea..fb89916b4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,15 +38,31 @@ public abstract class TemplatedSnippet implements Snippet { private final String snippetName; + private final String templateName; + /** * Creates a new {@code TemplatedSnippet} that will produce a snippet with the given - * {@code snippetName}. The given {@code attributes} will be included in the model - * during rendering of the template. - * - * @param snippetName The name of the snippet - * @param attributes The additional attributes + * {@code snippetName}. The {@code snippetName} will also be used as the name of the + * template. The given {@code attributes} will be included in the model during + * rendering of the template. + * @param snippetName the name of the snippet + * @param attributes the additional attributes + * @see #TemplatedSnippet(String, String, Map) */ protected TemplatedSnippet(String snippetName, Map attributes) { + this(snippetName, snippetName, attributes); + } + + /** + * Creates a new {@code TemplatedSnippet} that will produce a snippet with the given + * {@code snippetName} using a template with the given {@code templateName}. The given + * {@code attributes} will be included in the model during rendering of the template. + * @param snippetName the name of the snippet + * @param templateName the name of the template + * @param attributes the additional attributes + */ + protected TemplatedSnippet(String snippetName, String templateName, Map attributes) { + this.templateName = templateName; this.snippetName = snippetName; if (attributes != null) { this.attributes.putAll(attributes); @@ -55,17 +71,15 @@ protected TemplatedSnippet(String snippetName, Map attributes) { @Override public void document(Operation operation) throws IOException { - RestDocumentationContext context = (RestDocumentationContext) operation - .getAttributes().get(RestDocumentationContext.class.getName()); - WriterResolver writerResolver = (WriterResolver) operation.getAttributes() - .get(WriterResolver.class.getName()); - try (Writer writer = writerResolver.resolve(operation.getName(), this.snippetName, - context)) { - Map model = createModel(operation); - model.putAll(this.attributes); + RestDocumentationContext context = (RestDocumentationContext) operation.getAttributes() + .get(RestDocumentationContext.class.getName()); + WriterResolver writerResolver = (WriterResolver) operation.getAttributes().get(WriterResolver.class.getName()); + Map model = createModel(operation); + model.putAll(this.attributes); + try (Writer writer = writerResolver.resolve(operation.getName(), this.snippetName, context)) { TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes() - .get(TemplateEngine.class.getName()); - writer.append(templateEngine.compileTemplate(this.snippetName).render(model)); + .get(TemplateEngine.class.getName()); + writer.append(templateEngine.compileTemplate(this.templateName).render(model)); } } @@ -74,8 +88,7 @@ public void document(Operation operation) throws IOException { * given {@code operation}. Any additional attributes that were supplied when this * {@code TemplatedSnippet} were created will be automatically added to the model * prior to rendering. - * - * @param operation The operation + * @param operation the operation * @return the model * @throws ModelCreationException if model creation fails */ @@ -84,7 +97,6 @@ public void document(Operation operation) throws IOException { /** * Returns the additional attributes that will be included in the model during * template rendering. - * * @return the additional attributes */ protected final Map getAttributes() { @@ -93,7 +105,6 @@ protected final Map getAttributes() { /** * Returns the name of the snippet that will be created. - * * @return the snippet name */ protected final String getSnippetName() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java index 89bbe5a23..90c3577be 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,25 +32,13 @@ public interface WriterResolver { /** * Returns a writer that can be used to write the snippet with the given name for the * operation with the given name. - * * @param operationName the name of the operation that is being documented * @param snippetName the name of the snippet * @param restDocumentationContext the current documentation context * @return the writer * @throws IOException if a writer cannot be resolved */ - Writer resolve(String operationName, String snippetName, - RestDocumentationContext restDocumentationContext) throws IOException; - - /** - * Configures the encoding that should be used by any writers produced by this - * resolver. - * - * @param encoding the encoding - * @deprecated since 1.1.0 in favour of configuring the encoding when to resolver is - * created - */ - @Deprecated - void setEncoding(String encoding); + Writer resolve(String operationName, String snippetName, RestDocumentationContext restDocumentationContext) + throws IOException; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java index d7900f9b0..087985e6a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,22 +41,9 @@ public class StandardTemplateResourceResolver implements TemplateResourceResolve private final TemplateFormat templateFormat; - /** - * Creates a new {@code StandardTemplateResourceResolver} that will produce default - * template resources formatted with Asciidoctor. - * - * @deprecated since 1.1.0 in favour of - * {@link #StandardTemplateResourceResolver(TemplateFormat)} - */ - @Deprecated - public StandardTemplateResourceResolver() { - this(TemplateFormats.asciidoctor()); - } - /** * Creates a new {@code StandardTemplateResourceResolver} that will produce default * template resources formatted with the given {@code templateFormat}. - * * @param templateFormat the format for the default snippet templates */ public StandardTemplateResourceResolver(TemplateFormat templateFormat) { @@ -77,24 +64,20 @@ public Resource resolveTemplateResource(String name) { if (defaultTemplate.exists()) { return defaultTemplate; } - throw new IllegalStateException( - "Template named '" + name + "' could not be resolved"); + throw new IllegalStateException("Template named '" + name + "' could not be resolved"); } private Resource getFormatSpecificCustomTemplate(String name) { - return new ClassPathResource( - String.format("org/springframework/restdocs/templates/%s/%s.snippet", - this.templateFormat.getId(), name)); + return new ClassPathResource(String.format("org/springframework/restdocs/templates/%s/%s.snippet", + this.templateFormat.getId(), name)); } private Resource getCustomTemplate(String name) { - return new ClassPathResource( - String.format("org/springframework/restdocs/templates/%s.snippet", name)); + return new ClassPathResource(String.format("org/springframework/restdocs/templates/%s.snippet", name)); } private Resource getDefaultTemplate(String name) { - return new ClassPathResource(String.format( - "org/springframework/restdocs/templates/%s/default-%s.snippet", + return new ClassPathResource(String.format("org/springframework/restdocs/templates/%s/default-%s.snippet", this.templateFormat.getId(), name)); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/Template.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/Template.java index 47c7f5b91..182a86505 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/Template.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/Template.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,9 +29,8 @@ public interface Template { /** * Renders the template to a {@link String} using the given {@code context} for * variable/property resolution. - * - * @param context The context to use - * @return The rendered template + * @param context the context to use + * @return the rendered template */ String render(Map context); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java index d628937bc..5c328d1f6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java @@ -32,7 +32,6 @@ public interface TemplateEngine { * Compiles the template at the given {@code path}. Typically, a * {@link TemplateResourceResolver} will be used to resolve the path into a resource * that can be read and compiled. - * * @param path the path of the template * @return the compiled {@code Template} * @throws IOException if compilation fails diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java index 41bca73ad..820a3cd09 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java @@ -27,7 +27,6 @@ public interface TemplateFormat { /** * Returns the id of this template format. - * * @return the id */ String getId(); @@ -35,7 +34,6 @@ public interface TemplateFormat { /** * Returns the file extension to use for files generated from templates in this * format. - * * @return the file extension */ String getFileExtension(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java index 3c9ad9019..aeb7c1452 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ package org.springframework.restdocs.templates; /** - * An enumeration of the built-in formats for which templates are provuded. + * An enumeration of the built-in formats for which templates are provided. * * @author Andy Wilkinson * @since 1.1.0 @@ -35,7 +35,6 @@ private TemplateFormats() { /** * Returns the Asciidoctor template format with the ID {@code asciidoctor} and the * file extension {@code adoc}. - * * @return the template format */ public static TemplateFormat asciidoctor() { @@ -45,7 +44,6 @@ public static TemplateFormat asciidoctor() { /** * Returns the Markdown template format with the ID {@code markdown} and the file * extension {@code md}. - * * @return the template format */ public static TemplateFormat markdown() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java index cd308638e..5e80193ce 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java @@ -28,7 +28,6 @@ public interface TemplateResourceResolver { /** * Resolves a {@link Resource} for the template with the given {@code name}. - * * @param name the name of the template * @return the {@code Resource} from which the template can be read */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java index c63bcf3f2..3fc9e3a41 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,7 @@ public class MustacheTemplate implements Template { /** * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. - * - * @param delegate The delegate to adapt + * @param delegate the delegate to adapt */ public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate) { this(delegate, Collections.emptyMap()); @@ -48,12 +47,10 @@ public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate) * During rendering, the given {@code context} and the context passed into * {@link #render(Map)} will be combined and then passed to the delegate when it is * {@link org.springframework.restdocs.mustache.Template#execute executed}. - * - * @param delegate The delegate to adapt - * @param context The context + * @param delegate the delegate to adapt + * @param context the context */ - public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate, - Map context) { + public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate, Map context) { this.delegate = delegate; this.context = context; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java index 76a33d6f4..584c871a5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.io.IOException; import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; @@ -40,65 +42,108 @@ public class MustacheTemplateEngine implements TemplateEngine { private final TemplateResourceResolver templateResourceResolver; + private final Charset templateEncoding; + private final Compiler compiler; private final Map context; /** * Creates a new {@code MustacheTemplateEngine} that will use the given - * {@code templateResourceResolver} to resolve template paths. - * + * {@code templateResourceResolver} to resolve template paths. Templates will be read + * as UTF-8. * @param templateResourceResolver the resolver to use */ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) { this(templateResourceResolver, Mustache.compiler().escapeHTML(false)); } + /** + * Creates a new {@code MustacheTemplateEngine} that will use the given + * {@code templateResourceResolver} to resolve template paths, reading them using the + * given {@code templateEncoding}. + * @param templateResourceResolver the resolver to use + * @param templateEncoding the charset to use when reading the templates + * @since 2.0.5 + */ + public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, Charset templateEncoding) { + this(templateResourceResolver, templateEncoding, Mustache.compiler().escapeHTML(false)); + } + /** * Creates a new {@code MustacheTemplateEngine} that will use the given * {@code templateResourceResolver} to resolve templates and the given - * {@code compiler} to compile them. - * + * {@code compiler} to compile them. Templates will be read as UTF-8. * @param templateResourceResolver the resolver to use * @param compiler the compiler to use */ - public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, - Compiler compiler) { + public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, Compiler compiler) { this(templateResourceResolver, compiler, Collections.emptyMap()); } /** * Creates a new {@code MustacheTemplateEngine} that will use the given * {@code templateResourceResolver} to resolve templates and the given - * {@code compiler} to compile them. Compiled templates will be created with the given + * {@code compiler} to compile them. Templates will be read using the given + * {@code templateEncoding}. + * @param templateResourceResolver the resolver to use + * @param templateEncoding the charset to use when reading the templates + * @param compiler the compiler to use + * @since 2.0.5 + */ + public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, Charset templateEncoding, + Compiler compiler) { + this(templateResourceResolver, templateEncoding, compiler, Collections.emptyMap()); + } + + /** + * Creates a new {@code MustacheTemplateEngine} that will use the given + * {@code templateResourceResolver} to resolve templates. Templates will be read as + * UTF-8. Once read, the given {@code compiler} will be used to compile them. Compiled + * templates will be created with the given {@code context}. + * @param templateResourceResolver the resolver to use + * @param compiler the compiler to use + * @param context the context to pass to compiled templates + * @see MustacheTemplate#MustacheTemplate(org.springframework.restdocs.mustache.Template, + * Map) + */ + public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, Compiler compiler, + Map context) { + this(templateResourceResolver, StandardCharsets.UTF_8, compiler, context); + } + + /** + * Creates a new {@code MustacheTemplateEngine} that will use the given + * {@code templateResourceResolver} to resolve templates. Template will be read using + * the given {@code templateEncoding}. Once read, the given {@code compiler} will be + * used to compile them. Compiled templates will be created with the given * {@code context}. - * * @param templateResourceResolver the resolver to use + * @param templateEncoding the charset to use when reading the templates * @param compiler the compiler to use * @param context the context to pass to compiled templates + * @since 2.0.5 * @see MustacheTemplate#MustacheTemplate(org.springframework.restdocs.mustache.Template, * Map) */ - public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, + public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, Charset templateEncoding, Compiler compiler, Map context) { this.templateResourceResolver = templateResourceResolver; + this.templateEncoding = templateEncoding; this.compiler = compiler; this.context = context; } @Override public Template compileTemplate(String name) throws IOException { - Resource templateResource = this.templateResourceResolver - .resolveTemplateResource(name); + Resource templateResource = this.templateResourceResolver.resolveTemplateResource(name); return new MustacheTemplate( - this.compiler.compile( - new InputStreamReader(templateResource.getInputStream())), + this.compiler.compile(new InputStreamReader(templateResource.getInputStream(), this.templateEncoding)), this.context); } /** * Returns the {@link Compiler} used to compile Mustache templates. - * * @return the compiler */ protected final Compiler getCompiler() { @@ -108,7 +153,6 @@ protected final Compiler getCompiler() { /** * Returns the {@link TemplateResourceResolver} used to resolve the template resources * prior to compilation. - * * @return the resolver */ protected final TemplateResourceResolver getTemplateResourceResolver() { diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties index d42b5ae68..699b900e2 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties @@ -1,25 +1,32 @@ -javax.validation.constraints.AssertFalse.description=Must be false -javax.validation.constraints.AssertTrue.description=Must be true -javax.validation.constraints.DecimalMax.description=Must be at most ${value} -javax.validation.constraints.DecimalMin.description=Must be at least ${value} -javax.validation.constraints.Digits.description=Must have at most ${integer} integral digits and ${fraction} fractional digits -javax.validation.constraints.Future.description=Must be in the future -javax.validation.constraints.Max.description=Must be at most ${value} -javax.validation.constraints.Min.description=Must be at least ${value} -javax.validation.constraints.NotNull.description=Must not be null -javax.validation.constraints.Null.description=Must be null -javax.validation.constraints.Past.description=Must be in the past -javax.validation.constraints.Pattern.description=Must match the regular expression `${regexp}` -javax.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive +jakarta.validation.constraints.AssertFalse.description=Must be false +jakarta.validation.constraints.AssertTrue.description=Must be true +jakarta.validation.constraints.DecimalMax.description=Must be at most ${value} +jakarta.validation.constraints.DecimalMin.description=Must be at least ${value} +jakarta.validation.constraints.Digits.description=Must have at most ${integer} integral digits and ${fraction} fractional digits +jakarta.validation.constraints.Email.description=Must be a well-formed email address +jakarta.validation.constraints.Future.description=Must be in the future +jakarta.validation.constraints.FutureOrPresent.description=Must be in the future or the present +jakarta.validation.constraints.Max.description=Must be at most ${value} +jakarta.validation.constraints.Min.description=Must be at least ${value} +jakarta.validation.constraints.Negative.description=Must be negative +jakarta.validation.constraints.NegativeOrZero.description=Must be negative or zero +jakarta.validation.constraints.NotBlank.description=Must not be blank +jakarta.validation.constraints.NotEmpty.description=Must not be empty +jakarta.validation.constraints.NotNull.description=Must not be null +jakarta.validation.constraints.Null.description=Must be null +jakarta.validation.constraints.Past.description=Must be in the past +jakarta.validation.constraints.PastOrPresent.description=Must be in the past or the present +jakarta.validation.constraints.Pattern.description=Must match the regular expression `${regexp}` +jakarta.validation.constraints.Positive.description=Must be positive +jakarta.validation.constraints.PositiveOrZero.description=Must be positive or zero +jakarta.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive +org.hibernate.validator.constraints.CodePointLength.description=Code point length must be between ${min} and ${max} inclusive org.hibernate.validator.constraints.CreditCardNumber.description=Must be a well-formed credit card number +org.hibernate.validator.constraints.Currency.description=Must be in an accepted currency unit (${value}) org.hibernate.validator.constraints.EAN.description=Must be a well-formed ${type} number -org.hibernate.validator.constraints.Email.description=Must be a well-formed email address org.hibernate.validator.constraints.Length.description=Length must be between ${min} and ${max} inclusive org.hibernate.validator.constraints.LuhnCheck.description=Must pass the Luhn Modulo 10 checksum algorithm org.hibernate.validator.constraints.Mod10Check.description=Must pass the Mod10 checksum algorithm org.hibernate.validator.constraints.Mod11Check.description=Must pass the Mod11 checksum algorithm -org.hibernate.validator.constraints.NotBlank.description=Must not be blank -org.hibernate.validator.constraints.NotEmpty.description=Must not be empty org.hibernate.validator.constraints.Range.description=Must be at least ${min} and at most ${max} -org.hibernate.validator.constraints.SafeHtml.description=Must be safe HTML org.hibernate.validator.constraints.URL.description=Must be a well-formed URL \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-form-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-form-parameters.snippet new file mode 100644 index 000000000..9e6f6888a --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-form-parameters.snippet @@ -0,0 +1,9 @@ +|=== +|Parameter|Description + +{{#parameters}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet index ab50823d7..fda4f8376 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet @@ -2,7 +2,7 @@ |Relation|Description {{#links}} -|{{#tableCellContent}}`{{rel}}`{{/tableCellContent}} +|{{#tableCellContent}}`+{{rel}}+`{{/tableCellContent}} |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/links}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet index c318e4019..6976b7b28 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet @@ -1,9 +1,9 @@ -.{{path}} +.+{{path}}+ |=== |Parameter|Description {{#parameters}} -|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/parameters}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-query-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-query-parameters.snippet new file mode 100644 index 000000000..9e6f6888a --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-query-parameters.snippet @@ -0,0 +1,9 @@ +|=== +|Parameter|Description + +{{#parameters}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet new file mode 100644 index 000000000..00da2d085 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet @@ -0,0 +1,4 @@ +[source{{#language}},{{language}}{{/language}},options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-cookies.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-cookies.snippet new file mode 100644 index 000000000..0c5315051 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-cookies.snippet @@ -0,0 +1,9 @@ +|=== +|Name|Description + +{{#cookies}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/cookies}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet index 46cd43fe4..0d8f18e93 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet @@ -2,8 +2,8 @@ |Path|Type|Description {{#fields}} -|{{#tableCellContent}}`{{path}}`{{/tableCellContent}} -|{{#tableCellContent}}`{{type}}`{{/tableCellContent}} +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/fields}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet index 790a81bce..5a8593332 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet @@ -2,7 +2,7 @@ |Name|Description {{#headers}} -|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/headers}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet index 411c33c54..9e6f6888a 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet @@ -2,7 +2,7 @@ |Parameter|Description {{#parameters}} -|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/parameters}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet new file mode 100644 index 000000000..00da2d085 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet @@ -0,0 +1,4 @@ +[source{{#language}},{{language}}{{/language}},options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet new file mode 100644 index 000000000..0d8f18e93 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet @@ -0,0 +1,10 @@ +|=== +|Path|Type|Description + +{{#fields}} +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet index 06a65c1e2..23a23436c 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet @@ -2,7 +2,7 @@ |Part|Description {{#requestParts}} -|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/requestParts}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet new file mode 100644 index 000000000..00da2d085 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet @@ -0,0 +1,4 @@ +[source{{#language}},{{language}}{{/language}},options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-cookies.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-cookies.snippet new file mode 100644 index 000000000..0c5315051 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-cookies.snippet @@ -0,0 +1,9 @@ +|=== +|Name|Description + +{{#cookies}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/cookies}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet index 46cd43fe4..0d8f18e93 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet @@ -2,8 +2,8 @@ |Path|Type|Description {{#fields}} -|{{#tableCellContent}}`{{path}}`{{/tableCellContent}} -|{{#tableCellContent}}`{{type}}`{{/tableCellContent}} +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/fields}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet index 790a81bce..5a8593332 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet @@ -2,7 +2,7 @@ |Name|Description {{#headers}} -|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} |{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/headers}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-form-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-form-parameters.snippet new file mode 100644 index 000000000..681daaa8e --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-form-parameters.snippet @@ -0,0 +1,5 @@ +Parameter | Description +--------- | ----------- +{{#parameters}} +`{{name}}` | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-query-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-query-parameters.snippet new file mode 100644 index 000000000..681daaa8e --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-query-parameters.snippet @@ -0,0 +1,5 @@ +Parameter | Description +--------- | ----------- +{{#parameters}} +`{{name}}` | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet new file mode 100644 index 000000000..6abf3f7e8 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet @@ -0,0 +1,3 @@ +```{{#language}}{{language}}{{/language}} +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-cookies.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-cookies.snippet new file mode 100644 index 000000000..dbc046b82 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-cookies.snippet @@ -0,0 +1,5 @@ +Name | Description +---- | ----------- +{{#cookies}} +`{{name}}` | {{description}} +{{/cookies}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet new file mode 100644 index 000000000..6abf3f7e8 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet @@ -0,0 +1,3 @@ +```{{#language}}{{language}}{{/language}} +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet new file mode 100644 index 000000000..27a4e4379 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet @@ -0,0 +1,5 @@ +Path | Type | Description +---- | ---- | ----------- +{{#fields}} +`{{path}}` | `{{type}}` | {{description}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet new file mode 100644 index 000000000..6abf3f7e8 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet @@ -0,0 +1,3 @@ +```{{#language}}{{language}}{{/language}} +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-cookies.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-cookies.snippet new file mode 100644 index 000000000..dbc046b82 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-cookies.snippet @@ -0,0 +1,5 @@ +Name | Description +---- | ----------- +{{#cookies}} +`{{name}}` | {{description}} +{{/cookies}} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index aba259639..bc0a5d09e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,81 +16,60 @@ package org.springframework.restdocs; -import java.util.Arrays; -import java.util.List; - -import org.junit.Rule; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpStatus; import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; -import org.springframework.restdocs.test.SnippetMatchers; -import org.springframework.restdocs.test.SnippetMatchers.CodeBlockMatcher; -import org.springframework.restdocs.test.SnippetMatchers.HttpRequestMatcher; -import org.springframework.restdocs.test.SnippetMatchers.HttpResponseMatcher; -import org.springframework.restdocs.test.SnippetMatchers.TableMatcher; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.testfixtures.SnippetConditions; +import org.springframework.restdocs.testfixtures.SnippetConditions.CodeBlockCondition; +import org.springframework.restdocs.testfixtures.SnippetConditions.HttpRequestCondition; +import org.springframework.restdocs.testfixtures.SnippetConditions.HttpResponseCondition; +import org.springframework.restdocs.testfixtures.SnippetConditions.TableCondition; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; import org.springframework.web.bind.annotation.RequestMethod; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; -import static org.springframework.restdocs.templates.TemplateFormats.markdown; - /** * Abstract base class for testing snippet generation. * * @author Andy Wilkinson */ -@RunWith(Parameterized.class) public abstract class AbstractSnippetTests { - protected final TemplateFormat templateFormat; - - @Rule - public ExpectedSnippet snippet; + protected final TemplateFormat templateFormat = TemplateFormats.asciidoctor(); - @Rule - public OperationBuilder operationBuilder; + protected AssertableSnippets snippets; - @Parameters(name = "{0}") - public static List parameters() { - return Arrays.asList(new Object[] { "Asciidoctor", asciidoctor() }, - new Object[] { "Markdown", markdown() }); + public CodeBlockCondition codeBlock(String language) { + return this.codeBlock(language, null); } - public AbstractSnippetTests(String name, TemplateFormat templateFormat) { - this.snippet = new ExpectedSnippet(templateFormat); - this.templateFormat = templateFormat; - this.operationBuilder = new OperationBuilder(this.templateFormat); + public CodeBlockCondition codeBlock(String language, String options) { + return SnippetConditions.codeBlock(this.templateFormat, language, options); } - public CodeBlockMatcher codeBlock(String language) { - return SnippetMatchers.codeBlock(this.templateFormat, language); + public TableCondition tableWithHeader(String... headers) { + return SnippetConditions.tableWithHeader(this.templateFormat, headers); } - public TableMatcher tableWithHeader(String... headers) { - return SnippetMatchers.tableWithHeader(this.templateFormat, headers); + public TableCondition tableWithTitleAndHeader(String title, String... headers) { + return SnippetConditions.tableWithTitleAndHeader(this.templateFormat, title, headers); } - public TableMatcher tableWithTitleAndHeader(String title, String... headers) { - return SnippetMatchers.tableWithTitleAndHeader(this.templateFormat, title, - headers); + public HttpRequestCondition httpRequest(RequestMethod method, String uri) { + return SnippetConditions.httpRequest(this.templateFormat, method, uri); } - public HttpRequestMatcher httpRequest(RequestMethod method, String uri) { - return SnippetMatchers.httpRequest(this.templateFormat, method, uri); + public HttpResponseCondition httpResponse(HttpStatus responseStatus) { + return SnippetConditions.httpResponse(this.templateFormat, responseStatus); } - public HttpResponseMatcher httpResponse(HttpStatus responseStatus) { - return SnippetMatchers.httpResponse(this.templateFormat, responseStatus); + public HttpResponseCondition httpResponse(int responseStatusCode) { + return SnippetConditions.httpResponse(this.templateFormat, responseStatusCode, ""); } protected FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + this.templateFormat.getId() + "/" + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + this.templateFormat.getId() + "/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java index 51853148d..3b41392a1 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,11 +22,13 @@ import java.util.HashMap; import java.util.Map; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; import org.mockito.Mockito; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; @@ -35,13 +37,13 @@ import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.RequestConverter; import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.operation.preprocess.OperationPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.operation.preprocess.Preprocessors; import org.springframework.restdocs.snippet.Snippet; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -51,122 +53,153 @@ * Tests for {@link RestDocumentationGenerator}. * * @author Andy Wilkinson + * @author Filip Hrisafov */ -public class RestDocumentationGeneratorTests { +class RestDocumentationGeneratorTests { @SuppressWarnings("unchecked") - private final RequestConverter requestConverter = mock( - RequestConverter.class); + private final RequestConverter requestConverter = mock(RequestConverter.class); @SuppressWarnings("unchecked") - private final ResponseConverter responseConverter = mock( - ResponseConverter.class); + private final ResponseConverter responseConverter = mock(ResponseConverter.class); private final Object request = new Object(); private final Object response = new Object(); private final OperationRequest operationRequest = new OperationRequestFactory() - .create(URI.create("http://localhost:8080"), null, null, new HttpHeaders(), - null, null); + .create(URI.create("http://localhost:8080"), null, null, new HttpHeaders(), null, null); - private final OperationResponse operationResponse = new OperationResponseFactory() - .create(null, null, null); + private final OperationResponse operationResponse = new OperationResponseFactory().create(HttpStatus.OK, null, + null); private final Snippet snippet = mock(Snippet.class); + private final OperationPreprocessor requestPreprocessor = mock(OperationPreprocessor.class); + + private final OperationPreprocessor responsePreprocessor = mock(OperationPreprocessor.class); + @Test - public void basicHandling() throws IOException { - given(this.requestConverter.convert(this.request)) - .willReturn(this.operationRequest); - given(this.responseConverter.convert(this.response)) - .willReturn(this.operationResponse); + void basicHandling() throws IOException { + given(this.requestConverter.convert(this.request)).willReturn(this.operationRequest); + given(this.responseConverter.convert(this.response)).willReturn(this.operationResponse); HashMap configuration = new HashMap<>(); - new RestDocumentationGenerator<>("id", this.requestConverter, - this.responseConverter, this.snippet).handle(this.request, this.response, - configuration); + new RestDocumentationGenerator<>("id", this.requestConverter, this.responseConverter, this.snippet) + .handle(this.request, this.response, configuration); verifySnippetInvocation(this.snippet, configuration); } @Test - public void defaultSnippetsAreCalled() throws IOException { - given(this.requestConverter.convert(this.request)) - .willReturn(this.operationRequest); - given(this.responseConverter.convert(this.response)) - .willReturn(this.operationResponse); + void defaultSnippetsAreCalled() throws IOException { + given(this.requestConverter.convert(this.request)).willReturn(this.operationRequest); + given(this.responseConverter.convert(this.response)).willReturn(this.operationResponse); HashMap configuration = new HashMap<>(); Snippet defaultSnippet1 = mock(Snippet.class); Snippet defaultSnippet2 = mock(Snippet.class); configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS, Arrays.asList(defaultSnippet1, defaultSnippet2)); - new RestDocumentationGenerator<>("id", this.requestConverter, - this.responseConverter, this.snippet).handle(this.request, this.response, - configuration); - verifySnippetInvocation(this.snippet, configuration); - verifySnippetInvocation(defaultSnippet1, configuration); - verifySnippetInvocation(defaultSnippet2, configuration); + new RestDocumentationGenerator<>("id", this.requestConverter, this.responseConverter, this.snippet) + .handle(this.request, this.response, configuration); + InOrder inOrder = Mockito.inOrder(defaultSnippet1, defaultSnippet2, this.snippet); + verifySnippetInvocation(inOrder, defaultSnippet1, configuration); + verifySnippetInvocation(inOrder, defaultSnippet2, configuration); + verifySnippetInvocation(inOrder, this.snippet, configuration); } @Test - @Deprecated - public void additionalSnippetsAreCalled() throws IOException { - given(this.requestConverter.convert(this.request)) - .willReturn(this.operationRequest); - given(this.responseConverter.convert(this.response)) - .willReturn(this.operationResponse); - Snippet additionalSnippet1 = mock(Snippet.class); - Snippet additionalSnippet2 = mock(Snippet.class); - RestDocumentationGenerator generator = new RestDocumentationGenerator<>( - "id", this.requestConverter, this.responseConverter, this.snippet); - generator.addSnippets(additionalSnippet1, additionalSnippet2); + void defaultOperationRequestPreprocessorsAreCalled() throws IOException { + given(this.requestConverter.convert(this.request)).willReturn(this.operationRequest); + given(this.responseConverter.convert(this.response)).willReturn(this.operationResponse); HashMap configuration = new HashMap<>(); - generator.handle(this.request, this.response, configuration); - generator.handle(this.request, this.response, configuration); - verifySnippetInvocation(this.snippet, configuration, 2); - verifySnippetInvocation(additionalSnippet1, configuration); - verifySnippetInvocation(additionalSnippet2, configuration); + OperationPreprocessor defaultPreprocessor1 = mock(OperationPreprocessor.class); + OperationPreprocessor defaultPreprocessor2 = mock(OperationPreprocessor.class); + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_REQUEST_PREPROCESSOR, + Preprocessors.preprocessRequest(defaultPreprocessor1, defaultPreprocessor2)); + OperationRequest first = createRequest(); + OperationRequest second = createRequest(); + OperationRequest third = createRequest(); + given(this.requestPreprocessor.preprocess(this.operationRequest)).willReturn(first); + given(defaultPreprocessor1.preprocess(first)).willReturn(second); + given(defaultPreprocessor2.preprocess(second)).willReturn(third); + new RestDocumentationGenerator<>("id", this.requestConverter, this.responseConverter, + Preprocessors.preprocessRequest(this.requestPreprocessor), this.snippet) + .handle(this.request, this.response, configuration); + verifySnippetInvocation(this.snippet, third, this.operationResponse, configuration, 1); + } + + @Test + void defaultOperationResponsePreprocessorsAreCalled() throws IOException { + given(this.requestConverter.convert(this.request)).willReturn(this.operationRequest); + given(this.responseConverter.convert(this.response)).willReturn(this.operationResponse); + HashMap configuration = new HashMap<>(); + OperationPreprocessor defaultPreprocessor1 = mock(OperationPreprocessor.class); + OperationPreprocessor defaultPreprocessor2 = mock(OperationPreprocessor.class); + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR, + Preprocessors.preprocessResponse(defaultPreprocessor1, defaultPreprocessor2)); + OperationResponse first = createResponse(); + OperationResponse second = createResponse(); + OperationResponse third = new OperationResponseFactory().createFrom(this.operationResponse, new HttpHeaders()); + given(this.responsePreprocessor.preprocess(this.operationResponse)).willReturn(first); + given(defaultPreprocessor1.preprocess(first)).willReturn(second); + given(defaultPreprocessor2.preprocess(second)).willReturn(third); + new RestDocumentationGenerator<>("id", this.requestConverter, this.responseConverter, + Preprocessors.preprocessResponse(this.responsePreprocessor), this.snippet) + .handle(this.request, this.response, configuration); + verifySnippetInvocation(this.snippet, this.operationRequest, third, configuration, 1); } @Test - public void newGeneratorOnlyCallsItsSnippets() throws IOException { - OperationRequestPreprocessor requestPreprocessor = mock( - OperationRequestPreprocessor.class); - OperationResponsePreprocessor responsePreprocessor = mock( - OperationResponsePreprocessor.class); - given(this.requestConverter.convert(this.request)) - .willReturn(this.operationRequest); - given(this.responseConverter.convert(this.response)) - .willReturn(this.operationResponse); - given(requestPreprocessor.preprocess(this.operationRequest)) - .willReturn(this.operationRequest); - given(responsePreprocessor.preprocess(this.operationResponse)) - .willReturn(this.operationResponse); + void newGeneratorOnlyCallsItsSnippets() throws IOException { + OperationRequestPreprocessor requestPreprocessor = mock(OperationRequestPreprocessor.class); + OperationResponsePreprocessor responsePreprocessor = mock(OperationResponsePreprocessor.class); + given(this.requestConverter.convert(this.request)).willReturn(this.operationRequest); + given(this.responseConverter.convert(this.response)).willReturn(this.operationResponse); + given(requestPreprocessor.preprocess(this.operationRequest)).willReturn(this.operationRequest); + given(responsePreprocessor.preprocess(this.operationResponse)).willReturn(this.operationResponse); Snippet additionalSnippet1 = mock(Snippet.class); Snippet additionalSnippet2 = mock(Snippet.class); - RestDocumentationGenerator generator = new RestDocumentationGenerator<>( - "id", this.requestConverter, this.responseConverter, requestPreprocessor, - responsePreprocessor, this.snippet); + RestDocumentationGenerator generator = new RestDocumentationGenerator<>("id", + this.requestConverter, this.responseConverter, requestPreprocessor, responsePreprocessor, this.snippet); HashMap configuration = new HashMap<>(); generator.withSnippets(additionalSnippet1, additionalSnippet2) - .handle(this.request, this.response, configuration); + .handle(this.request, this.response, configuration); verifyNoMoreInteractions(this.snippet); verifySnippetInvocation(additionalSnippet1, configuration); verifySnippetInvocation(additionalSnippet2, configuration); } - private void verifySnippetInvocation(Snippet snippet, Map attributes) - throws IOException { - verifySnippetInvocation(snippet, attributes, 1); + private void verifySnippetInvocation(Snippet snippet, Map attributes) throws IOException { + ArgumentCaptor operation = ArgumentCaptor.forClass(Operation.class); + verify(snippet).document(operation.capture()); + assertThat(this.operationRequest).isEqualTo(operation.getValue().getRequest()); + assertThat(this.operationResponse).isEqualTo(operation.getValue().getResponse()); + assertThat(attributes).isEqualTo(operation.getValue().getAttributes()); } - private void verifySnippetInvocation(Snippet snippet, Map attributes, - int times) throws IOException { + private void verifySnippetInvocation(Snippet snippet, OperationRequest operationRequest, + OperationResponse operationResponse, Map attributes, int times) throws IOException { ArgumentCaptor operation = ArgumentCaptor.forClass(Operation.class); verify(snippet, Mockito.times(times)).document(operation.capture()); - assertThat(this.operationRequest, is(equalTo(operation.getValue().getRequest()))); - assertThat(this.operationResponse, - is(equalTo(operation.getValue().getResponse()))); - assertThat(attributes, is(equalTo(operation.getValue().getAttributes()))); + assertThat(operationRequest).isEqualTo(operation.getValue().getRequest()); + assertThat(operationResponse).isEqualTo(operation.getValue().getResponse()); + } + + private void verifySnippetInvocation(InOrder inOrder, Snippet snippet, Map attributes) + throws IOException { + ArgumentCaptor operation = ArgumentCaptor.forClass(Operation.class); + inOrder.verify(snippet).document(operation.capture()); + assertThat(this.operationRequest).isEqualTo(operation.getValue().getRequest()); + assertThat(this.operationResponse).isEqualTo(operation.getValue().getResponse()); + assertThat(attributes).isEqualTo(operation.getValue().getAttributes()); + } + + private static OperationRequest createRequest() { + return new OperationRequestFactory().create(URI.create("http://localhost:8080"), null, null, new HttpHeaders(), + null, null); + } + + private static OperationResponse createResponse() { + return new OperationResponseFactory().create(HttpStatus.OK, null, null); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/ConcatenatingCommandFormatterTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/ConcatenatingCommandFormatterTests.java new file mode 100644 index 000000000..c69c879bb --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/ConcatenatingCommandFormatterTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.cli; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CommandFormatter}. + * + * @author Tomasz Kopczynski + * @author Andy Wilkinson + */ +class ConcatenatingCommandFormatterTests { + + private CommandFormatter singleLineFormat = new ConcatenatingCommandFormatter(" "); + + @Test + void formattingAnEmptyListProducesAnEmptyString() { + assertThat(this.singleLineFormat.format(Collections.emptyList())).isEqualTo(""); + } + + @Test + void formattingNullProducesAnEmptyString() { + assertThat(this.singleLineFormat.format(null)).isEqualTo(""); + } + + @Test + void formattingASingleElement() { + assertThat(this.singleLineFormat.format(Collections.singletonList("alpha"))).isEqualTo(" alpha"); + } + + @Test + void formattingMultipleElements() { + assertThat(this.singleLineFormat.format(Arrays.asList("alpha", "bravo"))).isEqualTo(" alpha bravo"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index 55eb317af..e0136a1dd 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,25 +17,15 @@ package org.springframework.restdocs.cli; import java.io.IOException; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import java.util.Base64; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.util.Base64Utils; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; + +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link CurlRequestSnippet}. @@ -45,326 +35,265 @@ * @author Dmitriy Mayboroda * @author Jonathan Pearlin * @author Paul-Christian Volkmer + * @author Tomasz Kopczynski */ -@RunWith(Parameterized.class) -public class CurlRequestSnippetTests extends AbstractSnippetTests { - - public CurlRequestSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } - - @Test - public void getRequest() throws IOException { - this.snippet.expectCurlRequest().withContents( - codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); - new CurlRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo").build()); - } +class CurlRequestSnippetTests { - @Test - public void getRequestWithParameter() throws IOException { - this.snippet.expectCurlRequest().withContents( - codeBlock("bash").content("$ curl 'http://localhost/foo?a=alpha' -i")); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").param("a", "alpha").build()); - } + private CommandFormatter commandFormatter = CliDocumentation.singleLineFormat(); - @Test - public void nonGetRequest() throws IOException { - this.snippet.expectCurlRequest().withContents( - codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").method("POST").build()); + @RenderedSnippetTest + void getRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo").build()); + assertThat(snippets.curlRequest()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguage("bash").content("$ curl 'http://localhost/foo' -i -X GET")); } - @Test - public void requestWithContent() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -d 'content'")); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").content("content").build()); + @RenderedSnippetTest + void nonGetRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo").method("POST").build()); + assertThat(snippets.curlRequest()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguage("bash").content("$ curl 'http://localhost/foo' -i -X POST")); } - @Test - public void getRequestWithQueryString() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param=value' -i")); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo?param=value").build()); + @RenderedSnippetTest + void requestWithContent(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo").content("content").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X GET -d 'content'")); } - @Test - public void getRequestWithTotallyOverlappingQueryStringAndParameters() - throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param=value' -i")); - new CurlRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo?param=value") - .param("param", "value").build()); + @RenderedSnippetTest + void getRequestWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo?param=value").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo?param=value' -i -X GET")); } - @Test - public void getRequestWithPartiallyOverlappingQueryStringAndParameters() + @RenderedSnippetTest + void getRequestWithQueryStringWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); - new CurlRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo?a=alpha") - .param("a", "alpha").param("b", "bravo").build()); + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo?param").build()); + assertThat(snippets.curlRequest()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguage("bash").content("$ curl 'http://localhost/foo?param' -i -X GET")); } - @Test - public void getRequestWithDisjointQueryStringAndParameters() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo?a=alpha").param("b", "bravo").build()); + @RenderedSnippetTest + void postRequestWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo?param=value").method("POST").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo?param=value' -i -X POST")); } - @Test - public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectCurlRequest().withContents( - codeBlock("bash").content("$ curl 'http://localhost/foo?param' -i")); - new CurlRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo?param").build()); + @RenderedSnippetTest + void postRequestWithQueryStringWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo?param").method("POST").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo?param' -i -X POST")); } - @Test - public void postRequestWithQueryString() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param=value' -i -X POST")); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo?param=value").method("POST").build()); + @RenderedSnippetTest + void postRequestWithOneParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo").method("POST").content("k1=v1").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); } - @Test - public void postRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param' -i -X POST")); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo?param").method("POST").build()); + @RenderedSnippetTest + void postRequestWithOneParameterAndExplicitContentType(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .method("POST") + .content("k1=v1") + .build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); } - @Test - public void postRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); - new CurlRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .method("POST").param("k1", "v1").build()); + @RenderedSnippetTest + void postRequestWithOneParameterWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo").method("POST").content("k1=").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); } - @Test - public void postRequestWithOneParameterWithNoValue() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").method("POST").param("k1").build()); + @RenderedSnippetTest + void postRequestWithMultipleParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .method("POST") + .content("k1=v1&k1=v1-bis&k2=v2") + .build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); } - @Test - public void postRequestWithMultipleParameters() throws IOException { - this.snippet.expectCurlRequest() - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X POST" - + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo").method("POST") - .param("k1", "v1", "v1-bis").param("k2", "v2").build()); + @RenderedSnippetTest + void postRequestWithUrlEncodedParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo").method("POST").content("k1=a%26b").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); } - @Test - public void postRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); - new CurlRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .method("POST").param("k1", "a&b").build()); + @RenderedSnippetTest + void postRequestWithJsonData(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .content("{\"a\":\"alpha\"}") + .build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content( + "$ curl 'http://localhost/foo' -i -X POST -H 'Content-Type: application/json' -d '{\"a\":\"alpha\"}'")); } - @Test - public void postRequestWithDisjointQueryStringAndParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo?a=alpha") - .method("POST").param("b", "bravo").build()); + @RenderedSnippetTest + void putRequestWithOneParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo").method("PUT").content("k1=v1").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); } - @Test - public void postRequestWithTotallyOverlappingQueryStringAndParameters() + @RenderedSnippetTest + void putRequestWithMultipleParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i -X POST")); - new CurlRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") - .method("POST").param("a", "alpha").param("b", "bravo").build()); + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .method("PUT") + .content("k1=v1&k1=v1-bis&k2=v2") + .build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); } - @Test - public void postRequestWithPartiallyOverlappingQueryStringAndParameters() + @RenderedSnippetTest + void putRequestWithUrlEncodedParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo?a=alpha") - .method("POST").param("a", "alpha").param("b", "bravo").build()); - } - - @Test - public void putRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo").method("PUT").content("k1=a%26b").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); } - @Test - public void putRequestWithMultipleParameters() throws IOException { - this.snippet.expectCurlRequest() - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X PUT" - + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .param("k1", "v1-bis").param("k2", "v2").build()); + @RenderedSnippetTest + void requestWithHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha") + .build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X GET" + " -H 'Content-Type: application/json' -H 'a: alpha'")); } - @Test - public void putRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); - new CurlRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .method("PUT").param("k1", "a&b").build()); - } - - @Test - public void requestWithHeaders() throws IOException { - this.snippet.expectCurlRequest() - .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" - + " -H 'Content-Type: application/json' -H 'a: alpha'")); - new CurlRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_JSON_VALUE) - .header("a", "alpha").build()); + @RenderedSnippetTest + void requestWithHeadersMultiline(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(CliDocumentation.multiLineFormat()) + .document(operationBuilder.request("http://localhost/foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha") + .build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content(String.format("$ curl 'http://localhost/foo' -i -X GET \\%n" + + " -H 'Content-Type: application/json' \\%n" + " -H 'a: alpha'"))); } - @Test - public void multipartPostWithNoSubmittedFileName() throws IOException { - String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " - + "'Content-Type: multipart/form-data' -F " - + "'metadata={\"description\": \"foo\"}'"; - this.snippet.expectCurlRequest() - .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); + @RenderedSnippetTest + void requestWithCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .cookie("name1", "value1") + .cookie("name2", "value2") + .build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X GET" + " --cookie 'name1=value1;name2=value2'")); } - @Test - public void multipartPostWithContentType() throws IOException { + @RenderedSnippetTest + void multipartPostWithNoSubmittedFileName(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()) + .build()); String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " - + "'Content-Type: multipart/form-data' -F " - + "'image=@documents/images/example.png;type=image/png'"; - this.snippet.expectCurlRequest() - .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document( - this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .submittedFileName("documents/images/example.png").build()); + + "'Content-Type: multipart/form-data' -F " + "'metadata={\"description\": \"foo\"}'"; + assertThat(snippets.curlRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content(expectedContent)); } - @Test - public void multipartPost() throws IOException { + @RenderedSnippetTest + void multipartPostWithContentType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png") + .build()); String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " - + "'Content-Type: multipart/form-data' -F " - + "'image=@documents/images/example.png'"; - this.snippet.expectCurlRequest() - .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document( - this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").build()); + + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png;type=image/png'"; + assertThat(snippets.curlRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content(expectedContent)); } - @Test - public void multipartPostWithParameters() throws IOException { + @RenderedSnippetTest + void multipartPost(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png") + .build()); String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " - + "'Content-Type: multipart/form-data' -F " - + "'image=@documents/images/example.png' -F 'a=apple' -F 'a=avocado' " - + "-F 'b=banana'"; - this.snippet.expectCurlRequest() - .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document( - this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").and() - .param("a", "apple", "avocado").param("b", "banana").build()); + + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png'"; + assertThat(snippets.curlRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content(expectedContent)); } - @Test - public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -u 'user:secret'")); - new CurlRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .header(HttpHeaders.AUTHORIZATION, - "Basic " + Base64Utils - .encodeToString("user:secret".getBytes())) - .build()); - } - - @Test - public void customAttributes() throws IOException { - this.snippet.expectCurlRequest() - .withContents(containsString("curl request title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("curl-request")) - .willReturn(snippetResource("curl-request-with-title")); - new CurlRequestSnippet( - attributes( - key("title").value("curl request title"))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost/foo").build()); + @RenderedSnippetTest + void basicAuthCredentialsAreSuppliedUsingUserOption(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString("user:secret".getBytes())) + .build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -u 'user:secret' -X GET")); } - @Test - public void customHostHeaderIsIncluded() throws IOException { - this.snippet.expectCurlRequest() - .withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i" + " -H 'Host: api.example.com'" - + " -H 'Content-Type: application/json' -H 'a: alpha'")); - new CurlRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .header(HttpHeaders.HOST, "api.example.com") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_JSON_VALUE) - .header("a", "alpha").build()); + @RenderedSnippetTest + void customAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .header(HttpHeaders.HOST, "api.example.com") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha") + .build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo' -i -X GET -H 'Host: api.example.com'" + + " -H 'Content-Type: application/json' -H 'a: alpha'")); } - @Test - public void postWithContentAndParameters() throws IOException { - this.snippet.expectCurlRequest() - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i " - + "-X POST -d 'Some content'")); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").param("a", "alpha").method("POST") - .param("b", "bravo").content("Some content").build()); + @RenderedSnippetTest + void deleteWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new CurlRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo?a=alpha&b=bravo").method("DELETE").build()); + assertThat(snippets.curlRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i " + "-X DELETE")); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 8031a9edf..89500ef06 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,25 +17,15 @@ package org.springframework.restdocs.cli; import java.io.IOException; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import java.util.Base64; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.util.Base64Utils; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link HttpieRequestSnippet}. @@ -46,326 +36,259 @@ * @author Jonathan Pearlin * @author Paul-Christian Volkmer * @author Raman Gupta + * @author Tomasz Kopczynski */ -@RunWith(Parameterized.class) -public class HttpieRequestSnippetTests extends AbstractSnippetTests { - - public HttpieRequestSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class HttpieRequestSnippetTests { - @Test - public void getRequest() throws IOException { - this.snippet.expectHttpieRequest().withContents( - codeBlock("bash").content("$ http GET 'http://localhost/foo'")); - new HttpieRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo").build()); - } + private CommandFormatter commandFormatter = CliDocumentation.singleLineFormat(); - @Test - public void getRequestWithParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents( - codeBlock("bash").content("$ http GET 'http://localhost/foo?a=alpha'")); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").param("a", "alpha").build()); + @RenderedSnippetTest + void getRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo").build()); + assertThat(snippets.httpieRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content("$ http GET 'http://localhost/foo'")); } - @Test - public void nonGetRequest() throws IOException { - this.snippet.expectHttpieRequest().withContents( - codeBlock("bash").content("$ http POST 'http://localhost/foo'")); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").method("POST").build()); + @RenderedSnippetTest + void nonGetRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo").method("POST").build()); + assertThat(snippets.httpieRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content("$ http POST 'http://localhost/foo'")); } - @Test - public void requestWithContent() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ echo 'content' | http GET 'http://localhost/foo'")); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").content("content").build()); + @RenderedSnippetTest + void requestWithContent(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo").content("content").build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ echo 'content' | http GET 'http://localhost/foo'")); } - @Test - public void getRequestWithQueryString() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?param=value'")); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo?param=value").build()); - } - - @Test - public void getRequestWithTotallyOverlappingQueryStringAndParameters() - throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?param=value'")); - new HttpieRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo?param=value") - .param("param", "value").build()); + @RenderedSnippetTest + void getRequestWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo?param=value").build()); + assertThat(snippets.httpieRequest()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguage("bash").content("$ http GET 'http://localhost/foo?param=value'")); } - @Test - public void getRequestWithPartiallyOverlappingQueryStringAndParameters() + @RenderedSnippetTest + void getRequestWithQueryStringWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); - new HttpieRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo?a=alpha") - .param("a", "alpha").param("b", "bravo").build()); + new HttpieRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo?param").build()); + assertThat(snippets.httpieRequest()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguage("bash").content("$ http GET 'http://localhost/foo?param'")); } - @Test - public void getRequestWithDisjointQueryStringAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo?a=alpha").param("b", "bravo").build()); + @RenderedSnippetTest + void postRequestWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo?param=value").method("POST").build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http POST 'http://localhost/foo?param=value'")); } - @Test - public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpieRequest().withContents( - codeBlock("bash").content("$ http GET 'http://localhost/foo?param'")); - new HttpieRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo?param").build()); - } - - @Test - public void postRequestWithQueryString() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http POST 'http://localhost/foo?param=value'")); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo?param=value").method("POST").build()); - } - - @Test - public void postRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpieRequest().withContents( - codeBlock("bash").content("$ http POST 'http://localhost/foo?param'")); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo?param").method("POST").build()); + @RenderedSnippetTest + void postRequestWithQueryStringWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo?param").method("POST").build()); + assertThat(snippets.httpieRequest()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguage("bash").content("$ http POST 'http://localhost/foo?param'")); } - @Test - public void postRequestWithOneParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http --form POST 'http://localhost/foo' 'k1=v1'")); - new HttpieRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .method("POST").param("k1", "v1").build()); + @RenderedSnippetTest + void postRequestWithOneParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .content("k1=v1") + .build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=v1'")); } - @Test - public void postRequestWithOneParameterWithNoValue() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http --form POST 'http://localhost/foo' 'k1='")); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").method("POST").param("k1").build()); + @RenderedSnippetTest + void postRequestWithOneParameterWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .content("k1") + .build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1='")); } - @Test - public void postRequestWithMultipleParameters() throws IOException { - this.snippet.expectHttpieRequest() - .withContents(codeBlock("bash") - .content("$ http --form POST 'http://localhost/foo'" - + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); - new HttpieRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo").method("POST") - .param("k1", "v1", "v1-bis").param("k2", "v2").build()); + @RenderedSnippetTest + void postRequestWithMultipleParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .content("k1=v1&k1=v1-bis&k2=v2") + .build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=v1' 'k1=v1-bis' 'k2=v2'")); } - @Test - public void postRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http --form POST 'http://localhost/foo' 'k1=a&b'")); - new HttpieRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .method("POST").param("k1", "a&b").build()); + @RenderedSnippetTest + void postRequestWithUrlEncodedParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .content("k1=a%26b") + .build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=a&b'")); } - @Test - public void postRequestWithDisjointQueryStringAndParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); - new HttpieRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo?a=alpha") - .method("POST").param("b", "bravo").build()); + @RenderedSnippetTest + void putRequestWithOneParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .method("PUT") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .content("k1=v1") + .build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); } - @Test - public void postRequestWithTotallyOverlappingQueryStringAndParameters() + @RenderedSnippetTest + void putRequestWithMultipleParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http POST 'http://localhost/foo?a=alpha&b=bravo'")); - new HttpieRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") - .method("POST").param("a", "alpha").param("b", "bravo").build()); + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .method("PUT") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .content("k1=v1&k1=v1-bis&k2=v2") + .build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form PUT 'http://localhost/foo'" + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); } - @Test - public void postRequestWithPartiallyOverlappingQueryStringAndParameters() + @RenderedSnippetTest + void putRequestWithUrlEncodedParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); - new HttpieRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo?a=alpha") - .method("POST").param("a", "alpha").param("b", "bravo").build()); + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .method("PUT") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .content("k1=a%26b") + .build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); } - @Test - public void putRequestWithOneParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); + @RenderedSnippetTest + void requestWithHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha") + .build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content(("$ http GET 'http://localhost/foo'" + " 'Content-Type:application/json' 'a:alpha'"))); } - @Test - public void putRequestWithMultipleParameters() throws IOException { - this.snippet.expectHttpieRequest() - .withContents(codeBlock("bash") - .content("$ http --form PUT 'http://localhost/foo'" - + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .param("k1", "v1-bis").param("k2", "v2").build()); - } - - @Test - public void putRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); - new HttpieRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .method("PUT").param("k1", "a&b").build()); + @RenderedSnippetTest + void requestWithHeadersMultiline(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(CliDocumentation.multiLineFormat()) + .document(operationBuilder.request("http://localhost/foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha") + .build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content(String.format("$ http GET 'http://localhost/foo' \\%n" + + " 'Content-Type:application/json' \\%n 'a:alpha'"))); } - @Test - public void requestWithHeaders() throws IOException { - this.snippet.expectHttpieRequest().withContents( - codeBlock("bash").content("$ http GET 'http://localhost/foo'" - + " 'Content-Type:application/json' 'a:alpha'")); - new HttpieRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_JSON_VALUE) - .header("a", "alpha").build()); + @RenderedSnippetTest + void requestWithCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .cookie("name1", "value1") + .cookie("name2", "value2") + .build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content(("$ http GET 'http://localhost/foo'" + " 'Cookie:name1=value1' 'Cookie:name2=value2'"))); } - @Test - public void multipartPostWithNoSubmittedFileName() throws IOException { - String expectedContent = String - .format("$ http --form POST 'http://localhost/upload' \\%n" - + " 'metadata'@<(echo '{\"description\": \"foo\"}')"); - this.snippet.expectHttpieRequest() - .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); + @RenderedSnippetTest + void multipartPostWithNoSubmittedFileName(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()) + .build()); + String expectedContent = "$ http --multipart POST 'http://localhost/upload'" + + " 'metadata'='{\"description\": \"foo\"}'"; + assertThat(snippets.httpieRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content(expectedContent)); } - @Test - public void multipartPostWithContentType() throws IOException { + @RenderedSnippetTest + void multipartPostWithContentType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png") + .build()); // httpie does not yet support manually set content type by part - String expectedContent = String - .format("$ http --form POST 'http://localhost/upload' \\%n" - + " 'image'@'documents/images/example.png'"); - this.snippet.expectHttpieRequest() - .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet().document( - this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .submittedFileName("documents/images/example.png").build()); - } - - @Test - public void multipartPost() throws IOException { - String expectedContent = String - .format("$ http --form POST 'http://localhost/upload' \\%n" - + " 'image'@'documents/images/example.png'"); - this.snippet.expectHttpieRequest() - .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet().document( - this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").build()); + String expectedContent = "$ http --multipart POST 'http://localhost/upload'" + + " 'image'@'documents/images/example.png'"; + assertThat(snippets.httpieRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content(expectedContent)); } - @Test - public void multipartPostWithParameters() throws IOException { - String expectedContent = String - .format("$ http --form POST 'http://localhost/upload' \\%n" - + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado'" - + " 'b=banana'"); - this.snippet.expectHttpieRequest() - .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet().document( - this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").and() - .param("a", "apple", "avocado").param("b", "banana").build()); + @RenderedSnippetTest + void multipartPost(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png") + .build()); + String expectedContent = "$ http --multipart POST 'http://localhost/upload'" + + " 'image'@'documents/images/example.png'"; + assertThat(snippets.httpieRequest()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash").content(expectedContent)); } - @Test - public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") - .content("$ http --auth 'user:secret' GET 'http://localhost/foo'")); - new HttpieRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .header(HttpHeaders.AUTHORIZATION, - "Basic " + Base64Utils - .encodeToString("user:secret".getBytes())) - .build()); - } - - @Test - public void customAttributes() throws IOException { - this.snippet.expectHttpieRequest() - .withContents(containsString("httpie request title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("httpie-request")) - .willReturn(snippetResource("httpie-request-with-title")); - new HttpieRequestSnippet( - attributes( - key("title").value("httpie request title"))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost/foo").build()); + @RenderedSnippetTest + void basicAuthCredentialsAreSuppliedUsingAuthOption(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString("user:secret".getBytes())) + .build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http --auth 'user:secret' GET 'http://localhost/foo'")); } - @Test - public void customHostHeaderIsIncluded() throws IOException { - this.snippet.expectHttpieRequest() - .withContents(codeBlock("bash").content( - "$ http GET 'http://localhost/foo' 'Host:api.example.com'" - + " 'Content-Type:application/json' 'a:alpha'")); - new HttpieRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .header(HttpHeaders.HOST, "api.example.com") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_JSON_VALUE) - .header("a", "alpha").build()); + @RenderedSnippetTest + void customAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter).document(operationBuilder.request("http://localhost/foo") + .header(HttpHeaders.HOST, "api.example.com") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha") + .build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http GET 'http://localhost/foo' 'Host:api.example.com'" + + " 'Content-Type:application/json' 'a:alpha'")); } - @Test - public void postWithContentAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents( - codeBlock("bash").content("$ echo 'Some content' | http POST " - + "'http://localhost/foo?a=alpha&b=bravo'")); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").method("POST").param("a", "alpha") - .param("b", "bravo").content("Some content").build()); + @RenderedSnippetTest + void deleteWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpieRequestSnippet(this.commandFormatter) + .document(operationBuilder.request("http://localhost/foo?a=alpha&b=bravo").method("DELETE").build()); + assertThat(snippets.httpieRequest()).isCodeBlock((codeBlock) -> codeBlock.withLanguage("bash") + .content("$ http DELETE 'http://localhost/foo?a=alpha&b=bravo'")); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index b339ef97b..aa6c68141 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,18 @@ package org.springframework.restdocs.config; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.hamcrest.Matchers; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.cli.CliDocumentation; @@ -31,6 +36,15 @@ import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.operation.preprocess.Preprocessors; +import org.springframework.restdocs.payload.RequestBodySnippet; +import org.springframework.restdocs.payload.ResponseBodySnippet; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; @@ -40,172 +54,192 @@ import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.test.util.ReflectionTestUtils; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.hasEntry; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; /** * Tests for {@link RestDocumentationConfigurer}. * * @author Andy Wilkinson + * @author Filip Hrisafov */ -public class RestDocumentationConfigurerTests { +class RestDocumentationConfigurerTests { private final TestRestDocumentationConfigurer configurer = new TestRestDocumentationConfigurer(); @SuppressWarnings("unchecked") @Test - public void defaultConfiguration() { + void defaultConfiguration() { Map configuration = new HashMap<>(); this.configurer.apply(configuration, createContext()); - assertThat(configuration, hasEntry(equalTo(TemplateEngine.class.getName()), - instanceOf(MustacheTemplateEngine.class))); - assertThat(configuration, hasEntry(equalTo(WriterResolver.class.getName()), - instanceOf(StandardWriterResolver.class))); - assertThat(configuration, - hasEntry( - equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), - instanceOf(List.class))); + assertThat(configuration).containsKey(TemplateEngine.class.getName()); + assertThat(configuration.get(TemplateEngine.class.getName())).isInstanceOf(MustacheTemplateEngine.class); + assertThat(configuration.get(TemplateEngine.class.getName())).hasFieldOrPropertyWithValue("templateEncoding", + StandardCharsets.UTF_8); + assertThat(configuration).containsKey(WriterResolver.class.getName()); + assertThat(configuration.get(WriterResolver.class.getName())).isInstanceOf(StandardWriterResolver.class); + assertThat(configuration).containsKey(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + assertThat(configuration.get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS)) + .isInstanceOf(List.class); List defaultSnippets = (List) configuration - .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); - assertThat(defaultSnippets, - contains(instanceOf(CurlRequestSnippet.class), - instanceOf(HttpieRequestSnippet.class), - instanceOf(HttpRequestSnippet.class), - instanceOf(HttpResponseSnippet.class))); - assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), - instanceOf(SnippetConfiguration.class))); + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + assertThat(defaultSnippets).extracting("class") + .containsExactlyInAnyOrder(CurlRequestSnippet.class, HttpieRequestSnippet.class, HttpRequestSnippet.class, + HttpResponseSnippet.class, RequestBodySnippet.class, ResponseBodySnippet.class); + assertThat(configuration).containsKey(SnippetConfiguration.class.getName()); + assertThat(configuration.get(SnippetConfiguration.class.getName())).isInstanceOf(SnippetConfiguration.class); SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration - .get(SnippetConfiguration.class.getName()); - assertThat(snippetConfiguration.getEncoding(), is(equalTo("UTF-8"))); - assertThat(snippetConfiguration.getTemplateFormat(), - is(equalTo(TemplateFormats.asciidoctor()))); + .get(SnippetConfiguration.class.getName()); + assertThat(snippetConfiguration.getEncoding()).isEqualTo("UTF-8"); + assertThat(snippetConfiguration.getTemplateFormat().getId()).isEqualTo(TemplateFormats.asciidoctor().getId()); + OperationRequestPreprocessor defaultOperationRequestPreprocessor = (OperationRequestPreprocessor) configuration + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_REQUEST_PREPROCESSOR); + assertThat(defaultOperationRequestPreprocessor).isNull(); + + OperationResponsePreprocessor defaultOperationResponsePreprocessor = (OperationResponsePreprocessor) configuration + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR); + assertThat(defaultOperationResponsePreprocessor).isNull(); } @Test - public void customTemplateEngine() { + void customTemplateEngine() { Map configuration = new HashMap<>(); TemplateEngine templateEngine = mock(TemplateEngine.class); - this.configurer.templateEngine(templateEngine).apply(configuration, - createContext()); - assertThat(configuration, Matchers.hasEntry( - TemplateEngine.class.getName(), templateEngine)); + this.configurer.templateEngine(templateEngine).apply(configuration, createContext()); + assertThat(configuration).containsEntry(TemplateEngine.class.getName(), templateEngine); } @Test - public void customWriterResolver() { + void customWriterResolver() { Map configuration = new HashMap<>(); WriterResolver writerResolver = mock(WriterResolver.class); - this.configurer.writerResolver(writerResolver).apply(configuration, - createContext()); - assertThat(configuration, Matchers.hasEntry( - WriterResolver.class.getName(), writerResolver)); + this.configurer.writerResolver(writerResolver).apply(configuration, createContext()); + assertThat(configuration).containsEntry(WriterResolver.class.getName(), writerResolver); } @Test - public void customDefaultSnippets() { + void customDefaultSnippets() { Map configuration = new HashMap<>(); - this.configurer.snippets().withDefaults(CliDocumentation.curlRequest()) - .apply(configuration, createContext()); - assertThat(configuration, - hasEntry( - equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), - instanceOf(List.class))); + this.configurer.snippets().withDefaults(CliDocumentation.curlRequest()).apply(configuration, createContext()); + assertThat(configuration).containsKey(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + assertThat(configuration.get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS)) + .isInstanceOf(List.class); @SuppressWarnings("unchecked") List defaultSnippets = (List) configuration - .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); - assertThat(defaultSnippets, contains(instanceOf(CurlRequestSnippet.class))); + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + assertThat(defaultSnippets).hasSize(1); + assertThat(defaultSnippets).hasOnlyElementsOfType(CurlRequestSnippet.class); } @SuppressWarnings("unchecked") @Test - public void additionalDefaultSnippets() { + void additionalDefaultSnippets() { Map configuration = new HashMap<>(); Snippet snippet = mock(Snippet.class); - this.configurer.snippets().withAdditionalDefaults(snippet).apply(configuration, - createContext()); - assertThat(configuration, - hasEntry( - equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), - instanceOf(List.class))); + this.configurer.snippets().withAdditionalDefaults(snippet).apply(configuration, createContext()); + assertThat(configuration).containsKey(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + assertThat(configuration.get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS)) + .isInstanceOf(List.class); List defaultSnippets = (List) configuration - .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); - assertThat(defaultSnippets, - contains(instanceOf(CurlRequestSnippet.class), - instanceOf(HttpieRequestSnippet.class), - instanceOf(HttpRequestSnippet.class), - instanceOf(HttpResponseSnippet.class), equalTo(snippet))); + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + assertThat(defaultSnippets).extracting("class") + .containsExactlyInAnyOrder(CurlRequestSnippet.class, HttpieRequestSnippet.class, HttpRequestSnippet.class, + HttpResponseSnippet.class, RequestBodySnippet.class, ResponseBodySnippet.class, snippet.getClass()); } @Test - public void customSnippetEncoding() { + void customSnippetEncoding() { Map configuration = new HashMap<>(); - this.configurer.snippets().withEncoding("ISO 8859-1").apply(configuration, - createContext()); - assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), - instanceOf(SnippetConfiguration.class))); + this.configurer.snippets().withEncoding("ISO-8859-1"); + this.configurer.apply(configuration, createContext()); + assertThat(configuration).containsKey(SnippetConfiguration.class.getName()); + assertThat(configuration.get(SnippetConfiguration.class.getName())).isInstanceOf(SnippetConfiguration.class); SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration - .get(SnippetConfiguration.class.getName()); - assertThat(snippetConfiguration.getEncoding(), is(equalTo("ISO 8859-1"))); + .get(SnippetConfiguration.class.getName()); + assertThat(snippetConfiguration.getEncoding()).isEqualTo(StandardCharsets.ISO_8859_1.displayName()); + assertThat(configuration.get(TemplateEngine.class.getName())).hasFieldOrPropertyWithValue("templateEncoding", + StandardCharsets.ISO_8859_1); } @Test - public void customTemplateFormat() { + void customTemplateFormat() { Map configuration = new HashMap<>(); - this.configurer.snippets().withTemplateFormat(TemplateFormats.markdown()) - .apply(configuration, createContext()); - assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), - instanceOf(SnippetConfiguration.class))); + this.configurer.snippets().withTemplateFormat(TemplateFormats.markdown()).apply(configuration, createContext()); + assertThat(configuration).containsKey(SnippetConfiguration.class.getName()); + assertThat(configuration.get(SnippetConfiguration.class.getName())).isInstanceOf(SnippetConfiguration.class); SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration - .get(SnippetConfiguration.class.getName()); - assertThat(snippetConfiguration.getTemplateFormat(), - is(equalTo(TemplateFormats.markdown()))); + .get(SnippetConfiguration.class.getName()); + assertThat(snippetConfiguration.getTemplateFormat().getId()).isEqualTo(TemplateFormats.markdown().getId()); } @SuppressWarnings("unchecked") @Test - public void asciidoctorTableCellContentLambaIsInstalledWhenUsingAsciidoctorTemplateFormat() { + void asciidoctorTableCellContentLambaIsInstalledWhenUsingAsciidoctorTemplateFormat() { Map configuration = new HashMap<>(); this.configurer.apply(configuration, createContext()); - TemplateEngine templateEngine = (TemplateEngine) configuration - .get(TemplateEngine.class.getName()); + TemplateEngine templateEngine = (TemplateEngine) configuration.get(TemplateEngine.class.getName()); MustacheTemplateEngine mustacheTemplateEngine = (MustacheTemplateEngine) templateEngine; - Map templateContext = (Map) ReflectionTestUtils - .getField(mustacheTemplateEngine, "context"); - assertThat(templateContext, hasEntry(equalTo("tableCellContent"), - instanceOf(AsciidoctorTableCellContentLambda.class))); + Map templateContext = (Map) ReflectionTestUtils.getField(mustacheTemplateEngine, + "context"); + assertThat(templateContext).containsKey("tableCellContent"); + assertThat(templateContext.get("tableCellContent")).isInstanceOf(AsciidoctorTableCellContentLambda.class); } @SuppressWarnings("unchecked") @Test - public void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidoctorTemplateFormat() { + void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidoctorTemplateFormat() { Map configuration = new HashMap<>(); this.configurer.snippetConfigurer.withTemplateFormat(TemplateFormats.markdown()); this.configurer.apply(configuration, createContext()); - TemplateEngine templateEngine = (TemplateEngine) configuration - .get(TemplateEngine.class.getName()); + TemplateEngine templateEngine = (TemplateEngine) configuration.get(TemplateEngine.class.getName()); MustacheTemplateEngine mustacheTemplateEngine = (MustacheTemplateEngine) templateEngine; - Map templateContext = (Map) ReflectionTestUtils - .getField(mustacheTemplateEngine, "context"); - assertThat(templateContext.size(), equalTo(0)); + Map templateContext = (Map) ReflectionTestUtils.getField(mustacheTemplateEngine, + "context"); + assertThat(templateContext.size()).isEqualTo(0); + } + + @Test + void customDefaultOperationRequestPreprocessor() { + Map configuration = new HashMap<>(); + this.configurer.operationPreprocessors() + .withRequestDefaults(Preprocessors.prettyPrint(), Preprocessors.modifyHeaders().remove("Foo")) + .apply(configuration, createContext()); + OperationRequestPreprocessor preprocessor = (OperationRequestPreprocessor) configuration + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_REQUEST_PREPROCESSOR); + HttpHeaders headers = new HttpHeaders(); + headers.add("Foo", "value"); + OperationRequest request = new OperationRequestFactory().create(URI.create("http://localhost:8080"), + HttpMethod.GET, null, headers, null, Collections.emptyList()); + assertThat(preprocessor.preprocess(request).getHeaders().headerNames()).doesNotContain("Foo"); + } + + @Test + void customDefaultOperationResponsePreprocessor() { + Map configuration = new HashMap<>(); + this.configurer.operationPreprocessors() + .withResponseDefaults(Preprocessors.prettyPrint(), Preprocessors.modifyHeaders().remove("Foo")) + .apply(configuration, createContext()); + OperationResponsePreprocessor preprocessor = (OperationResponsePreprocessor) configuration + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR); + HttpHeaders headers = new HttpHeaders(); + headers.add("Foo", "value"); + OperationResponse response = new OperationResponseFactory().create(HttpStatus.OK, headers, null); + assertThat(preprocessor.preprocess(response).getHeaders().headerNames()).doesNotContain("Foo"); } private RestDocumentationContext createContext() { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( - "build"); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation("build"); manualRestDocumentation.beforeTest(null, null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; } private static final class TestRestDocumentationConfigurer extends - RestDocumentationConfigurer { + RestDocumentationConfigurer { - private final TestSnippetConfigurer snippetConfigurer = new TestSnippetConfigurer( + private final TestSnippetConfigurer snippetConfigurer = new TestSnippetConfigurer(this); + + private final TestOperationPreprocessorsConfigurer operationPreprocessorsConfigurer = new TestOperationPreprocessorsConfigurer( this); @Override @@ -213,10 +247,15 @@ public TestSnippetConfigurer snippets() { return this.snippetConfigurer; } + @Override + public TestOperationPreprocessorsConfigurer operationPreprocessors() { + return this.operationPreprocessorsConfigurer; + } + } - private static final class TestSnippetConfigurer extends - SnippetConfigurer { + private static final class TestSnippetConfigurer + extends SnippetConfigurer { private TestSnippetConfigurer(TestRestDocumentationConfigurer parent) { super(parent); @@ -224,4 +263,13 @@ private TestSnippetConfigurer(TestRestDocumentationConfigurer parent) { } + private static final class TestOperationPreprocessorsConfigurer extends + OperationPreprocessorsConfigurer { + + protected TestOperationPreprocessorsConfigurer(TestRestDocumentationConfigurer parent) { + super(parent); + } + + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java index 76a1f3e8b..c5425c38c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,9 @@ import java.util.Arrays; import java.util.Collections; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.contains; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -33,42 +30,35 @@ * * @author Andy Wilkinson */ -public class ConstraintDescriptionsTests { +class ConstraintDescriptionsTests { private final ConstraintResolver constraintResolver = mock(ConstraintResolver.class); private final ConstraintDescriptionResolver constraintDescriptionResolver = mock( ConstraintDescriptionResolver.class); - private final ConstraintDescriptions constraintDescriptions = new ConstraintDescriptions( - Constrained.class, this.constraintResolver, - this.constraintDescriptionResolver); + private final ConstraintDescriptions constraintDescriptions = new ConstraintDescriptions(Constrained.class, + this.constraintResolver, this.constraintDescriptionResolver); @Test - public void descriptionsForConstraints() { - Constraint constraint1 = new Constraint("constraint1", - Collections.emptyMap()); - Constraint constraint2 = new Constraint("constraint2", - Collections.emptyMap()); + void descriptionsForConstraints() { + Constraint constraint1 = new Constraint("constraint1", Collections.emptyMap()); + Constraint constraint2 = new Constraint("constraint2", Collections.emptyMap()); given(this.constraintResolver.resolveForProperty("foo", Constrained.class)) - .willReturn(Arrays.asList(constraint1, constraint2)); - given(this.constraintDescriptionResolver.resolveDescription(constraint1)) - .willReturn("Bravo"); - given(this.constraintDescriptionResolver.resolveDescription(constraint2)) - .willReturn("Alpha"); - assertThat(this.constraintDescriptions.descriptionsForProperty("foo"), - contains("Alpha", "Bravo")); + .willReturn(Arrays.asList(constraint1, constraint2)); + given(this.constraintDescriptionResolver.resolveDescription(constraint1)).willReturn("Bravo"); + given(this.constraintDescriptionResolver.resolveDescription(constraint2)).willReturn("Alpha"); + assertThat(this.constraintDescriptions.descriptionsForProperty("foo")).containsExactly("Alpha", "Bravo"); } @Test - public void emptyListOfDescriptionsWhenThereAreNoConstraints() { + void emptyListOfDescriptionsWhenThereAreNoConstraints() { given(this.constraintResolver.resolveForProperty("foo", Constrained.class)) - .willReturn(Collections.emptyList()); - assertThat(this.constraintDescriptions.descriptionsForProperty("foo").size(), - is(equalTo(0))); + .willReturn(Collections.emptyList()); + assertThat(this.constraintDescriptions.descriptionsForProperty("foo").size()).isEqualTo(0); } - private static class Constrained { + private static final class Constrained { } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java index 3eaef32d4..7a977a9a3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,211 +21,244 @@ import java.net.URL; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.ListResourceBundle; import java.util.ResourceBundle; - -import javax.validation.constraints.AssertFalse; -import javax.validation.constraints.AssertTrue; -import javax.validation.constraints.DecimalMax; -import javax.validation.constraints.DecimalMin; -import javax.validation.constraints.Digits; -import javax.validation.constraints.Future; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Null; -import javax.validation.constraints.Past; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Size; - +import java.util.Set; + +import javax.money.MonetaryAmount; + +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Negative; +import jakarta.validation.constraints.NegativeOrZero; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Null; +import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; +import org.hibernate.validator.constraints.CodePointLength; import org.hibernate.validator.constraints.CreditCardNumber; +import org.hibernate.validator.constraints.Currency; import org.hibernate.validator.constraints.EAN; -import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.LuhnCheck; import org.hibernate.validator.constraints.Mod10Check; import org.hibernate.validator.constraints.Mod11Check; -import org.hibernate.validator.constraints.NotBlank; -import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.Range; -import org.hibernate.validator.constraints.SafeHtml; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link ResourceBundleConstraintDescriptionResolver}. * * @author Andy Wilkinson */ -public class ResourceBundleConstraintDescriptionResolverTests { +class ResourceBundleConstraintDescriptionResolverTests { private final ResourceBundleConstraintDescriptionResolver resolver = new ResourceBundleConstraintDescriptionResolver(); @Test - public void defaultMessageAssertFalse() { - assertThat(constraintDescriptionForField("assertFalse"), - is(equalTo("Must be false"))); + void defaultMessageAssertFalse() { + assertThat(constraintDescriptionForField("assertFalse")).isEqualTo("Must be false"); + } + + @Test + void defaultMessageAssertTrue() { + assertThat(constraintDescriptionForField("assertTrue")).isEqualTo("Must be true"); + } + + @Test + void defaultMessageCodePointLength() { + assertThat(constraintDescriptionForField("codePointLength")) + .isEqualTo("Code point length must be between 2 and 5 inclusive"); + } + + @Test + void defaultMessageCurrency() { + assertThat(constraintDescriptionForField("currency")) + .isEqualTo("Must be in an accepted currency unit (GBP, USD)"); + } + + @Test + void defaultMessageDecimalMax() { + assertThat(constraintDescriptionForField("decimalMax")).isEqualTo("Must be at most 9.875"); + } + + @Test + void defaultMessageDecimalMin() { + assertThat(constraintDescriptionForField("decimalMin")).isEqualTo("Must be at least 1.5"); + } + + @Test + void defaultMessageDigits() { + assertThat(constraintDescriptionForField("digits")) + .isEqualTo("Must have at most 2 integral digits and 5 fractional digits"); + } + + @Test + void defaultMessageFuture() { + assertThat(constraintDescriptionForField("future")).isEqualTo("Must be in the future"); + } + + @Test + void defaultMessageFutureOrPresent() { + assertThat(constraintDescriptionForField("futureOrPresent")).isEqualTo("Must be in the future or the present"); } @Test - public void defaultMessageAssertTrue() { - assertThat(constraintDescriptionForField("assertTrue"), - is(equalTo("Must be true"))); + void defaultMessageMax() { + assertThat(constraintDescriptionForField("max")).isEqualTo("Must be at most 10"); } @Test - public void defaultMessageDecimalMax() { - assertThat(constraintDescriptionForField("decimalMax"), - is(equalTo("Must be at most 9.875"))); + void defaultMessageMin() { + assertThat(constraintDescriptionForField("min")).isEqualTo("Must be at least 10"); } @Test - public void defaultMessageDecimalMin() { - assertThat(constraintDescriptionForField("decimalMin"), - is(equalTo("Must be at least 1.5"))); + void defaultMessageNotNull() { + assertThat(constraintDescriptionForField("notNull")).isEqualTo("Must not be null"); } @Test - public void defaultMessageDigits() { - assertThat(constraintDescriptionForField("digits"), is( - equalTo("Must have at most 2 integral digits and 5 fractional digits"))); + void defaultMessageNull() { + assertThat(constraintDescriptionForField("nul")).isEqualTo("Must be null"); } @Test - public void defaultMessageFuture() { - assertThat(constraintDescriptionForField("future"), - is(equalTo("Must be in the future"))); + void defaultMessagePast() { + assertThat(constraintDescriptionForField("past")).isEqualTo("Must be in the past"); } @Test - public void defaultMessageMax() { - assertThat(constraintDescriptionForField("max"), - is(equalTo("Must be at most 10"))); + void defaultMessagePastOrPresent() { + assertThat(constraintDescriptionForField("pastOrPresent")).isEqualTo("Must be in the past or the present"); } @Test - public void defaultMessageMin() { - assertThat(constraintDescriptionForField("min"), - is(equalTo("Must be at least 10"))); + void defaultMessagePattern() { + assertThat(constraintDescriptionForField("pattern")) + .isEqualTo("Must match the regular expression `[A-Z][a-z]+`"); } @Test - public void defaultMessageNotNull() { - assertThat(constraintDescriptionForField("notNull"), - is(equalTo("Must not be null"))); + void defaultMessageSize() { + assertThat(constraintDescriptionForField("size")).isEqualTo("Size must be between 2 and 10 inclusive"); } @Test - public void defaultMessageNull() { - assertThat(constraintDescriptionForField("nul"), is(equalTo("Must be null"))); + void defaultMessageCreditCardNumber() { + assertThat(constraintDescriptionForField("creditCardNumber")) + .isEqualTo("Must be a well-formed credit card number"); } @Test - public void defaultMessagePast() { - assertThat(constraintDescriptionForField("past"), - is(equalTo("Must be in the past"))); + void defaultMessageEan() { + assertThat(constraintDescriptionForField("ean")).isEqualTo("Must be a well-formed EAN13 number"); } @Test - public void defaultMessagePattern() { - assertThat(constraintDescriptionForField("pattern"), - is(equalTo("Must match the regular expression `[A-Z][a-z]+`"))); + void defaultMessageEmail() { + assertThat(constraintDescriptionForField("email")).isEqualTo("Must be a well-formed email address"); } @Test - public void defaultMessageSize() { - assertThat(constraintDescriptionForField("size"), - is(equalTo("Size must be between 2 and 10 inclusive"))); + void defaultMessageLength() { + assertThat(constraintDescriptionForField("length")).isEqualTo("Length must be between 2 and 10 inclusive"); } @Test - public void defaultMessageCreditCardNumber() { - assertThat(constraintDescriptionForField("creditCardNumber"), - is(equalTo("Must be a well-formed credit card number"))); + void defaultMessageLuhnCheck() { + assertThat(constraintDescriptionForField("luhnCheck")) + .isEqualTo("Must pass the Luhn Modulo 10 checksum algorithm"); } @Test - public void defaultMessageEan() { - assertThat(constraintDescriptionForField("ean"), - is(equalTo("Must be a well-formed EAN13 number"))); + void defaultMessageMod10Check() { + assertThat(constraintDescriptionForField("mod10Check")).isEqualTo("Must pass the Mod10 checksum algorithm"); } @Test - public void defaultMessageEmail() { - assertThat(constraintDescriptionForField("email"), - is(equalTo("Must be a well-formed email address"))); + void defaultMessageMod11Check() { + assertThat(constraintDescriptionForField("mod11Check")).isEqualTo("Must pass the Mod11 checksum algorithm"); } @Test - public void defaultMessageLength() { - assertThat(constraintDescriptionForField("length"), - is(equalTo("Length must be between 2 and 10 inclusive"))); + void defaultMessageNegative() { + assertThat(constraintDescriptionForField("negative")).isEqualTo("Must be negative"); } @Test - public void defaultMessageLuhnCheck() { - assertThat(constraintDescriptionForField("luhnCheck"), - is(equalTo("Must pass the Luhn Modulo 10 checksum algorithm"))); + void defaultMessageNegativeOrZero() { + assertThat(constraintDescriptionForField("negativeOrZero")).isEqualTo("Must be negative or zero"); } @Test - public void defaultMessageMod10Check() { - assertThat(constraintDescriptionForField("mod10Check"), - is(equalTo("Must pass the Mod10 checksum algorithm"))); + void defaultMessageNotBlank() { + assertThat(constraintDescriptionForField("notBlank")).isEqualTo("Must not be blank"); } @Test - public void defaultMessageMod11Check() { - assertThat(constraintDescriptionForField("mod11Check"), - is(equalTo("Must pass the Mod11 checksum algorithm"))); + void defaultMessageNotEmpty() { + assertThat(constraintDescriptionForField("notEmpty")).isEqualTo("Must not be empty"); } @Test - public void defaultMessageNotBlank() { - assertThat(constraintDescriptionForField("notBlank"), - is(equalTo("Must not be blank"))); + void defaultMessageNotEmptyHibernateValidator() { + assertThat(constraintDescriptionForField("notEmpty")).isEqualTo("Must not be empty"); } @Test - public void defaultMessageNotEmpty() { - assertThat(constraintDescriptionForField("notEmpty"), - is(equalTo("Must not be empty"))); + void defaultMessagePositive() { + assertThat(constraintDescriptionForField("positive")).isEqualTo("Must be positive"); } @Test - public void defaultMessageRange() { - assertThat(constraintDescriptionForField("range"), - is(equalTo("Must be at least 10 and at most 100"))); + void defaultMessagePositiveOrZero() { + assertThat(constraintDescriptionForField("positiveOrZero")).isEqualTo("Must be positive or zero"); } @Test - public void defaultMessageSafeHtml() { - assertThat(constraintDescriptionForField("safeHtml"), - is(equalTo("Must be safe HTML"))); + void defaultMessageRange() { + assertThat(constraintDescriptionForField("range")).isEqualTo("Must be at least 10 and at most 100"); } @Test - public void defaultMessageUrl() { - assertThat(constraintDescriptionForField("url"), - is(equalTo("Must be a well-formed URL"))); + void defaultMessageUrl() { + assertThat(constraintDescriptionForField("url")).isEqualTo("Must be a well-formed URL"); } @Test - public void customMessage() { + void customMessage() { Thread.currentThread().setContextClassLoader(new ClassLoader() { @Override public URL getResource(String name) { - if (name.startsWith( - "org/springframework/restdocs/constraints/ConstraintDescriptions")) { + if (name.startsWith("org/springframework/restdocs/constraints/ConstraintDescriptions")) { return super.getResource( "org/springframework/restdocs/constraints/TestConstraintDescriptions.properties"); } @@ -236,9 +269,8 @@ public URL getResource(String name) { try { String description = new ResourceBundleConstraintDescriptionResolver() - .resolveDescription(new Constraint(NotNull.class.getName(), - Collections.emptyMap())); - assertThat(description, is(equalTo("Should not be null"))); + .resolveDescription(new Constraint(NotNull.class.getName(), Collections.emptyMap())); + assertThat(description).isEqualTo("Should not be null"); } finally { @@ -247,20 +279,41 @@ public URL getResource(String name) { } @Test - public void customResourceBundle() { + void customResourceBundle() { ResourceBundle bundle = new ListResourceBundle() { @Override protected Object[][] getContents() { - return new String[][] { - { NotNull.class.getName() + ".description", "Not null" } }; + return new String[][] { { NotNull.class.getName() + ".description", "Not null" } }; } }; String description = new ResourceBundleConstraintDescriptionResolver(bundle) - .resolveDescription(new Constraint(NotNull.class.getName(), - Collections.emptyMap())); - assertThat(description, is(equalTo("Not null"))); + .resolveDescription(new Constraint(NotNull.class.getName(), Collections.emptyMap())); + assertThat(description).isEqualTo("Not null"); + } + + @Test + void allBeanValidationConstraintsAreTested() throws Exception { + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resolver.getResources("jakarta/validation/constraints/*.class"); + Set> beanValidationConstraints = new HashSet<>(); + for (Resource resource : resources) { + String className = ClassUtils.convertResourcePathToClassName(((ClassPathResource) resource).getPath()); + if (className.endsWith(".class")) { + className = className.substring(0, className.length() - 6); + } + Class type = Class.forName(className); + if (type.isAnnotation() && type.isAnnotationPresent(jakarta.validation.Constraint.class)) { + beanValidationConstraints.add(type); + } + } + ReflectionUtils.doWithFields(Constrained.class, (field) -> { + for (Annotation annotation : field.getAnnotations()) { + beanValidationConstraints.remove(annotation.annotationType()); + } + }); + assertThat(beanValidationConstraints).isEmpty(); } private String constraintDescriptionForField(String name) { @@ -268,14 +321,14 @@ private String constraintDescriptionForField(String name) { } private Constraint getConstraintFromField(String name) { - Annotation[] annotations = ReflectionUtils.findField(Constrained.class, name) - .getAnnotations(); - Assert.isTrue(annotations.length == 1); + Annotation[] annotations = ReflectionUtils.findField(Constrained.class, name).getAnnotations(); + Assert.isTrue(annotations.length == 1, + "The field '" + name + "' must have " + "exactly one @Constrained annotation"); return new Constraint(annotations[0].annotationType().getName(), AnnotationUtils.getAnnotationAttributes(annotations[0])); } - private static class Constrained { + private static final class Constrained { @AssertFalse private boolean assertFalse; @@ -283,6 +336,12 @@ private static class Constrained { @AssertTrue private boolean assertTrue; + @CodePointLength(min = 2, max = 5) + private String codePointLength; + + @Currency({ "GBP", "USD" }) + private MonetaryAmount currency; + @DecimalMax("9.875") private BigDecimal decimalMax; @@ -295,6 +354,9 @@ private static class Constrained { @Future private Date future; + @FutureOrPresent + private Date futureOrPresent; + @Max(10) private int max; @@ -310,6 +372,9 @@ private static class Constrained { @Past private Date past; + @PastOrPresent + private Date pastOrPresent; + @Pattern(regexp = "[A-Z][a-z]+") private String pattern; @@ -337,20 +402,30 @@ private static class Constrained { @Mod11Check private String mod11Check; + @Negative + private int negative; + + @NegativeOrZero + private int negativeOrZero; + @NotBlank private String notBlank; @NotEmpty private String notEmpty; + @Positive + private int positive; + + @PositiveOrZero + private int positiveOrZero; + @Range(min = 10, max = 100) private int range; - @SafeHtml - private String safeHtml; - @org.hibernate.validator.constraints.URL private String url; + } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java index 7b6155c3f..203817f81 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,69 +26,60 @@ import java.util.Map; import java.util.Map.Entry; -import javax.validation.Payload; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Null; -import javax.validation.constraints.Size; - -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; +import jakarta.validation.Payload; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Null; +import jakarta.validation.constraints.Size; +import org.assertj.core.api.Condition; +import org.assertj.core.description.TextDescription; import org.hibernate.validator.constraints.CompositionType; import org.hibernate.validator.constraints.ConstraintComposition; -import org.hibernate.validator.constraints.NotBlank; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link ValidatorConstraintResolver}. * * @author Andy Wilkinson */ -public class ValidatorConstraintResolverTests { +class ValidatorConstraintResolverTests { private final ValidatorConstraintResolver resolver = new ValidatorConstraintResolver(); @Test - public void singleFieldConstraint() { - List constraints = this.resolver.resolveForProperty("single", - ConstrainedFields.class); - assertThat(constraints, hasSize(1)); - assertThat(constraints.get(0).getName(), is(NotNull.class.getName())); + void singleFieldConstraint() { + List constraints = this.resolver.resolveForProperty("single", ConstrainedFields.class); + assertThat(constraints).hasSize(1); + assertThat(constraints.get(0).getName()).isEqualTo(NotNull.class.getName()); } - @SuppressWarnings("unchecked") @Test - public void multipleFieldConstraints() { - List constraints = this.resolver.resolveForProperty("multiple", - ConstrainedFields.class); - assertThat(constraints, hasSize(2)); - assertThat(constraints, containsInAnyOrder(constraint(NotNull.class), - constraint(Size.class).config("min", 8).config("max", 16))); + void multipleFieldConstraints() { + List constraints = this.resolver.resolveForProperty("multiple", ConstrainedFields.class); + assertThat(constraints).hasSize(2); + assertThat(constraints.get(0)).is(constraint(NotNull.class)); + assertThat(constraints.get(1)).is(constraint(Size.class).config("min", 8).config("max", 16)); } @Test - public void noFieldConstraints() { - List constraints = this.resolver.resolveForProperty("none", - ConstrainedFields.class); - assertThat(constraints, hasSize(0)); + void noFieldConstraints() { + List constraints = this.resolver.resolveForProperty("none", ConstrainedFields.class); + assertThat(constraints).hasSize(0); } @Test - public void compositeConstraint() { - List constraints = this.resolver.resolveForProperty("composite", - ConstrainedFields.class); - assertThat(constraints, hasSize(1)); + void compositeConstraint() { + List constraints = this.resolver.resolveForProperty("composite", ConstrainedFields.class); + assertThat(constraints).hasSize(1); } - private ConstraintMatcher constraint(final Class annotation) { - return new ConstraintMatcher(annotation); + private ConstraintCondition constraint(final Class annotation) { + return new ConstraintCondition(annotation); } - private static class ConstrainedFields { + private static final class ConstrainedFields { @NotNull private String single; @@ -102,6 +93,7 @@ private static class ConstrainedFields { @CompositeConstraint private String composite; + } @ConstraintComposition(CompositionType.OR) @@ -109,7 +101,7 @@ private static class ConstrainedFields { @NotBlank @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) - @javax.validation.Constraint(validatedBy = {}) + @jakarta.validation.Constraint(validatedBy = {}) private @interface CompositeConstraint { String message() default "Must be null or not blank"; @@ -120,43 +112,35 @@ private static class ConstrainedFields { } - private static final class ConstraintMatcher extends BaseMatcher { + private static final class ConstraintCondition extends Condition { private final Class annotation; private final Map configuration = new HashMap<>(); - private ConstraintMatcher(Class annotation) { + private ConstraintCondition(Class annotation) { this.annotation = annotation; + as(new TextDescription("Constraint named %s with configuration %s", this.annotation, this.configuration)); } - public ConstraintMatcher config(String key, Object value) { + private ConstraintCondition config(String key, Object value) { this.configuration.put(key, value); return this; } @Override - public boolean matches(Object item) { - if (!(item instanceof Constraint)) { - return false; - } - Constraint constraint = (Constraint) item; + public boolean matches(Constraint constraint) { if (!constraint.getName().equals(this.annotation.getName())) { return false; } for (Entry entry : this.configuration.entrySet()) { - if (!constraint.getConfiguration().get(entry.getKey()) - .equals(entry.getValue())) { + if (!constraint.getConfiguration().get(entry.getKey()).equals(entry.getValue())) { return false; } } return true; } - @Override - public void describeTo(Description description) { - description.appendText("Constraint named " + this.annotation.getName() - + " with configuration " + this.configuration); - } } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java new file mode 100644 index 000000000..febac6e00 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java @@ -0,0 +1,179 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.cookies; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link RequestCookiesSnippet}. + * + * @author Clyde Stubbs + * @author Andy Wilkinson + */ +class RequestCookiesSnippetTests { + + @RenderedSnippetTest + void requestWithCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestCookiesSnippet( + Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two"))) + .document(operationBuilder.request("http://localhost") + .cookie("tz", "Europe%2FLondon") + .cookie("logged_in", "true") + .build()); + assertThat(snippets.requestCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two")); + } + + @RenderedSnippetTest + void ignoredRequestCookie(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestCookiesSnippet( + Arrays.asList(cookieWithName("tz").ignored(), cookieWithName("logged_in").description("two"))) + .document(operationBuilder.request("http://localhost") + .cookie("tz", "Europe%2FLondon") + .cookie("logged_in", "true") + .build()); + assertThat(snippets.requestCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`logged_in`", "two")); + } + + @RenderedSnippetTest + void allUndocumentedCookiesCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestCookiesSnippet( + Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two")), + true) + .document(operationBuilder.request("http://localhost") + .cookie("tz", "Europe%2FLondon") + .cookie("logged_in", "true") + .cookie("user_session", "abcd1234efgh5678") + .build()); + assertThat(snippets.requestCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two")); + } + + @RenderedSnippetTest + void missingOptionalCookie(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestCookiesSnippet(Arrays.asList(cookieWithName("tz").description("one").optional(), + cookieWithName("logged_in").description("two"))) + .document(operationBuilder.request("http://localhost").cookie("logged_in", "true").build()); + assertThat(snippets.requestCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two")); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-cookies", template = "request-cookies-with-title") + void requestCookiesWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestCookiesSnippet(Collections.singletonList(cookieWithName("tz").description("one")), + attributes(key("title").value("Custom title"))) + .document(operationBuilder.request("http://localhost").cookie("tz", "Europe%2FLondon").build()); + assertThat(snippets.requestCookies()).contains("Custom title"); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-cookies", template = "request-cookies-with-extra-column") + void requestCookiesWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestCookiesSnippet( + Arrays.asList(cookieWithName("tz").description("one").attributes(key("foo").value("alpha")), + cookieWithName("logged_in").description("two").attributes(key("foo").value("bravo")))) + .document(operationBuilder.request("http://localhost") + .cookie("tz", "Europe%2FLondon") + .cookie("logged_in", "true") + .build()); + assertThat(snippets.requestCookies()).isTable((table) -> table.withHeader("Name", "Description", "Foo") + .row("tz", "one", "alpha") + .row("logged_in", "two", "bravo")); + } + + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestCookiesSnippet( + Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two"))) + .and(cookieWithName("user_session").description("three")) + .document(operationBuilder.request("http://localhost") + .cookie("tz", "Europe%2FLondon") + .cookie("logged_in", "true") + .cookie("user_session", "abcd1234efgh5678") + .build()); + assertThat(snippets.requestCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`tz`", "one") + .row("`logged_in`", "two") + .row("`user_session`", "three")); + } + + @RenderedSnippetTest + void additionalDescriptorsWithRelaxedRequestCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestCookiesSnippet( + Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two")), + true) + .and(cookieWithName("user_session").description("three")) + .document(operationBuilder.request("http://localhost") + .cookie("tz", "Europe%2FLondon") + .cookie("logged_in", "true") + .cookie("user_session", "abcd1234efgh5678") + .cookie("color_theme", "light") + .build()); + assertThat(snippets.requestCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`tz`", "one") + .row("`logged_in`", "two") + .row("`user_session`", "three")); + } + + @RenderedSnippetTest + void tableCellContentIsEscapedWhenNecessary(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestCookiesSnippet(Collections.singletonList(cookieWithName("Foo|Bar").description("one|two"))) + .document(operationBuilder.request("http://localhost").cookie("Foo|Bar", "baz").build()); + assertThat(snippets.requestCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`Foo|Bar`", "one|two")); + } + + @SnippetTest + void missingRequestCookie(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestCookiesSnippet( + Collections.singletonList(CookieDocumentation.cookieWithName("JSESSIONID").description("one"))) + .document(operationBuilder.request("http://localhost").build())) + .withMessage("Cookies with the following names were not found in the request: [JSESSIONID]"); + } + + @SnippetTest + void undocumentedRequestCookie(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestCookiesSnippet(Collections.emptyList()).document( + operationBuilder.request("http://localhost").cookie("JSESSIONID", "1234abcd5678efgh").build())) + .withMessageEndingWith("Cookies with the following names were not documented: [JSESSIONID]"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java new file mode 100644 index 000000000..2d7883e45 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.cookies; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link ResponseCookiesSnippet}. + * + * @author Clyde Stubbs + * @author Andy Wilkinson + */ +class ResponseCookiesSnippetTests { + + @RenderedSnippetTest + void responseWithCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").description("one"), + cookieWithName("user_session").description("two"))) + .document(operationBuilder.response() + .cookie("has_recent_activity", "true") + .cookie("user_session", "1234abcd5678efgh") + .build()); + assertThat(snippets.responseCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`has_recent_activity`", "one") + .row("`user_session`", "two")); + } + + @RenderedSnippetTest + void ignoredResponseCookie(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").ignored(), + cookieWithName("user_session").description("two"))) + .document(operationBuilder.response() + .cookie("has_recent_activity", "true") + .cookie("user_session", "1234abcd5678efgh") + .build()); + assertThat(snippets.responseCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`user_session`", "two")); + } + + @RenderedSnippetTest + void allUndocumentedResponseCookiesCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").description("one"), + cookieWithName("user_session").description("two")), true) + .document(operationBuilder.response() + .cookie("has_recent_activity", "true") + .cookie("user_session", "1234abcd5678efgh") + .cookie("some_cookie", "value") + .build()); + assertThat(snippets.responseCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`has_recent_activity`", "one") + .row("`user_session`", "two")); + } + + @RenderedSnippetTest + void missingOptionalResponseCookie(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").description("one").optional(), + cookieWithName("user_session").description("two"))) + .document(operationBuilder.response().cookie("user_session", "1234abcd5678efgh").build()); + assertThat(snippets.responseCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`has_recent_activity`", "one") + .row("`user_session`", "two")); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-cookies", template = "response-cookies-with-title") + void responseCookiesWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseCookiesSnippet(Collections.singletonList(cookieWithName("has_recent_activity").description("one")), + attributes(key("title").value("Custom title"))) + .document(operationBuilder.response().cookie("has_recent_activity", "true").build()); + assertThat(snippets.responseCookies()).contains("Custom title"); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-cookies", template = "response-cookies-with-extra-column") + void responseCookiesWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseCookiesSnippet(Arrays.asList( + cookieWithName("has_recent_activity").description("one").attributes(key("foo").value("alpha")), + cookieWithName("user_session").description("two").attributes(key("foo").value("bravo")), + cookieWithName("color_theme").description("three").attributes(key("foo").value("charlie")))) + .document(operationBuilder.response() + .cookie("has_recent_activity", "true") + .cookie("user_session", "1234abcd5678efgh") + .cookie("color_theme", "high_contrast") + .build()); + assertThat(snippets.responseCookies()).isTable((table) -> table.withHeader("Name", "Description", "Foo") + .row("has_recent_activity", "one", "alpha") + .row("user_session", "two", "bravo") + .row("color_theme", "three", "charlie")); + } + + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + CookieDocumentation + .responseCookies(cookieWithName("has_recent_activity").description("one"), + cookieWithName("user_session").description("two")) + .and(cookieWithName("color_theme").description("three")) + .document(operationBuilder.response() + .cookie("has_recent_activity", "true") + .cookie("user_session", "1234abcd5678efgh") + .cookie("color_theme", "light") + .build()); + assertThat(snippets.responseCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`has_recent_activity`", "one") + .row("`user_session`", "two") + .row("`color_theme`", "three")); + } + + @RenderedSnippetTest + void additionalDescriptorsWithRelaxedResponseCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + CookieDocumentation.relaxedResponseCookies(cookieWithName("has_recent_activity").description("one")) + .and(cookieWithName("color_theme").description("two")) + .document(operationBuilder.response() + .cookie("has_recent_activity", "true") + .cookie("user_session", "1234abcd5678efgh") + .cookie("color_theme", "light") + .build()); + assertThat(snippets.responseCookies()).isTable((table) -> table.withHeader("Name", "Description") + .row("`has_recent_activity`", "one") + .row("`color_theme`", "two")); + } + + @RenderedSnippetTest + void tableCellContentIsEscapedWhenNecessary(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseCookiesSnippet(Collections.singletonList(cookieWithName("Foo|Bar").description("one|two"))) + .document(operationBuilder.response().cookie("Foo|Bar", "baz").build()); + assertThat(snippets.responseCookies()) + .isTable((table) -> table.withHeader("Name", "Description").row("`Foo|Bar`", "one|two")); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java deleted file mode 100644 index 5e39b6ba0..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.headers; - -import java.io.IOException; -import java.util.Arrays; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; - -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; - -/** - * Tests for failures when rendering {@link RequestHeadersSnippet} due to missing or - * undocumented headers. - * - * @author Andy Wilkinson - */ -public class RequestHeadersSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void missingRequestHeader() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Headers with the following names were not found" - + " in the request: [Accept]")); - new RequestHeadersSnippet( - Arrays.asList(headerWithName("Accept").description("one"))).document( - this.operationBuilder.request("http://localhost").build()); - } - - @Test - public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(endsWith("Headers with the following names were not found" - + " in the request: [Accept]")); - new RequestHeadersSnippet( - Arrays.asList(headerWithName("Accept").description("one"))) - .document(this.operationBuilder.request("http://localhost") - .header("X-Test", "test").build()); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 30aeba6ab..51f16ccc3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,18 +19,15 @@ import java.io.IOException; import java.util.Arrays; -import org.junit.Test; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -41,157 +38,125 @@ * @author Andreas Evers * @author Andy Wilkinson */ -public class RequestHeadersSnippetTests extends AbstractSnippetTests { - - public RequestHeadersSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); +class RequestHeadersSnippetTests { + + @RenderedSnippetTest + void requestWithHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"), + headerWithName("Accept").description("two"), headerWithName("Accept-Encoding").description("three"), + headerWithName("Accept-Language").description("four"), + headerWithName("Cache-Control").description("five"), headerWithName("Connection").description("six"))) + .document(operationBuilder.request("http://localhost") + .header("X-Test", "test") + .header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cache-Control", "max-age=0") + .header("Connection", "keep-alive") + .build()); + assertThat(snippets.requestHeaders()).isTable((table) -> table.withHeader("Name", "Description") + .row("`X-Test`", "one") + .row("`Accept`", "two") + .row("`Accept-Encoding`", "three") + .row("`Accept-Language`", "four") + .row("`Cache-Control`", "five") + .row("`Connection`", "six")); } - @Test - public void requestWithHeaders() throws IOException { - this.snippet.expectRequestHeaders() - .withContents(tableWithHeader("Name", "Description") - .row("`X-Test`", "one").row("`Accept`", "two") - .row("`Accept-Encoding`", "three") - .row("`Accept-Language`", "four").row("`Cache-Control`", "five") - .row("`Connection`", "six")); - new RequestHeadersSnippet( - Arrays.asList(headerWithName("X-Test").description("one"), - headerWithName("Accept").description("two"), - headerWithName("Accept-Encoding").description("three"), - headerWithName("Accept-Language").description("four"), - headerWithName("Cache-Control").description("five"), - headerWithName( - "Connection") - .description("six"))) - .document( - this.operationBuilder - .request( - "http://localhost") - .header("X-Test", "test") - .header("Accept", "*/*") - .header("Accept-Encoding", - "gzip, deflate") - .header("Accept-Language", - "en-US,en;q=0.5") - .header("Cache-Control", - "max-age=0") - .header("Connection", - "keep-alive") - .build()); + @RenderedSnippetTest + void caseInsensitiveRequestHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"))) + .document(operationBuilder.request("/").header("X-test", "test").build()); + assertThat(snippets.requestHeaders()) + .isTable((table) -> table.withHeader("Name", "Description").row("`X-Test`", "one")); } - @Test - public void caseInsensitiveRequestHeaders() throws IOException { - this.snippet.expectRequestHeaders().withContents( - tableWithHeader("Name", "Description").row("`X-Test`", "one")); - new RequestHeadersSnippet( - Arrays.asList(headerWithName("X-Test").description("one"))) - .document(this.operationBuilder.request("/") - .header("X-test", "test").build()); + @RenderedSnippetTest + void undocumentedRequestHeader(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"))).document( + operationBuilder.request("http://localhost").header("X-Test", "test").header("Accept", "*/*").build()); + assertThat(snippets.requestHeaders()) + .isTable((table) -> table.withHeader("Name", "Description").row("`X-Test`", "one")); } - @Test - public void undocumentedRequestHeader() throws IOException { - this.snippet.expectRequestHeaders().withContents( - tableWithHeader("Name", "Description").row("`X-Test`", "one")); - new RequestHeadersSnippet( - Arrays.asList(headerWithName("X-Test").description("one"))) - .document(this.operationBuilder.request("http://localhost") - .header("X-Test", "test").header("Accept", "*/*") - .build()); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-headers", template = "request-headers-with-title") + void requestHeadersWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one")), + attributes(key("title").value("Custom title"))) + .document(operationBuilder.request("http://localhost").header("X-Test", "test").build()); + assertThat(snippets.requestHeaders()).contains("Custom title"); } - @Test - public void requestHeadersWithCustomAttributes() throws IOException { - this.snippet.expectRequestHeaders().withContents(containsString("Custom title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-headers")) - .willReturn(snippetResource("request-headers-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-headers", template = "request-headers-with-extra-column") + void requestHeadersWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestHeadersSnippet( - Arrays.asList(headerWithName("X-Test").description("one")), - attributes( - key("title").value("Custom title"))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .header("X-Test", "test").build()); + Arrays.asList(headerWithName("X-Test").description("one").attributes(key("foo").value("alpha")), + headerWithName("Accept-Encoding").description("two").attributes(key("foo").value("bravo")), + headerWithName("Accept").description("three").attributes(key("foo").value("charlie")))) + .document(operationBuilder.request("http://localhost") + .header("X-Test", "test") + .header("Accept-Encoding", "gzip, deflate") + .header("Accept", "*/*") + .build()); + assertThat(snippets.requestHeaders()).isTable((table) -> table.withHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha") + .row("Accept-Encoding", "two", "bravo") + .row("Accept", "three", "charlie")); } - @Test - public void requestHeadersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectRequestHeaders().withContents(// - tableWithHeader("Name", "Description", "Foo") - .row("X-Test", "one", "alpha") - .row("Accept-Encoding", "two", "bravo") - .row("Accept", "three", "charlie")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-headers")) - .willReturn(snippetResource("request-headers-with-extra-column")); - new RequestHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one") - .attributes(key("foo").value("alpha")), - headerWithName("Accept-Encoding").description("two") - .attributes(key("foo").value("bravo")), - headerWithName("Accept").description("three") - .attributes(key("foo").value("charlie")))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .header("X-Test", "test") - .header("Accept-Encoding", - "gzip, deflate") - .header("Accept", "*/*").build()); + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + HeaderDocumentation + .requestHeaders(headerWithName("X-Test").description("one"), headerWithName("Accept").description("two"), + headerWithName("Accept-Encoding").description("three"), + headerWithName("Accept-Language").description("four")) + .and(headerWithName("Cache-Control").description("five"), headerWithName("Connection").description("six")) + .document(operationBuilder.request("http://localhost") + .header("X-Test", "test") + .header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cache-Control", "max-age=0") + .header("Connection", "keep-alive") + .build()); + assertThat(snippets.requestHeaders()).isTable((table) -> table.withHeader("Name", "Description") + .row("`X-Test`", "one") + .row("`Accept`", "two") + .row("`Accept-Encoding`", "three") + .row("`Accept-Language`", "four") + .row("`Cache-Control`", "five") + .row("`Connection`", "six")); } - @Test - public void additionalDescriptors() throws IOException { - this.snippet.expectRequestHeaders() - .withContents(tableWithHeader("Name", "Description") - .row("`X-Test`", "one").row("`Accept`", "two") - .row("`Accept-Encoding`", "three") - .row("`Accept-Language`", "four").row("`Cache-Control`", "five") - .row("`Connection`", "six")); - HeaderDocumentation - .requestHeaders(headerWithName("X-Test").description("one"), - headerWithName("Accept").description("two"), - headerWithName("Accept-Encoding").description("three"), - headerWithName("Accept-Language").description("four")) - .and(headerWithName("Cache-Control").description("five"), - headerWithName( - "Connection") - .description( - "six")) - .document(this.operationBuilder.request("http://localhost") - .header("X-Test", "test").header("Accept", "*/*") - .header("Accept-Encoding", "gzip, deflate") - .header("Accept-Language", "en-US,en;q=0.5") - .header("Cache-Control", "max-age=0") - .header("Connection", "keep-alive").build()); + @RenderedSnippetTest + void tableCellContentIsEscapedWhenNecessary(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestHeadersSnippet(Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) + .document(operationBuilder.request("http://localhost").header("Foo|Bar", "baz").build()); + assertThat(snippets.requestHeaders()) + .isTable((table) -> table.withHeader("Name", "Description").row("`Foo|Bar`", "one|two")); } - @Test - public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectRequestHeaders().withContents( - tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), - escapeIfNecessary("one|two"))); - new RequestHeadersSnippet( - Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) - .document(this.operationBuilder.request("http://localhost") - .header("Foo|Bar", "baz").build()); + @SnippetTest + void missingRequestHeader(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description("one"))) + .document(operationBuilder.request("http://localhost").build())) + .withMessage("Headers with the following names were not found in the request: [Accept]"); } - private String escapeIfNecessary(String input) { - if (this.templateFormat.equals(TemplateFormats.markdown())) { - return input; - } - return input.replace("|", "\\|"); + @SnippetTest + void undocumentedRequestHeaderAndMissingRequestHeader(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description("one"))) + .document(operationBuilder.request("http://localhost").header("X-Test", "test").build())) + .withMessageEndingWith("Headers with the following names were not found in the request: [Accept]"); + } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java deleted file mode 100644 index 5049224f1..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.headers; - -import java.io.IOException; -import java.util.Arrays; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; - -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; - -/** - * Tests for failures when rendering {@link ResponseHeadersSnippet} due to missing or - * undocumented headers. - * - * @author Andy Wilkinson - */ -public class ResponseHeadersSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void missingResponseHeader() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Headers with the following names were not found" - + " in the response: [Content-Type]")); - new ResponseHeadersSnippet( - Arrays.asList(headerWithName("Content-Type").description("one"))) - .document(this.operationBuilder.response().build()); - } - - @Test - public void undocumentedResponseHeaderAndMissingResponseHeader() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(endsWith("Headers with the following names were not found" - + " in the response: [Content-Type]")); - new ResponseHeadersSnippet( - Arrays.asList(headerWithName("Content-Type").description("one"))) - .document(this.operationBuilder.response() - .header("X-Test", "test").build()); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 702a28ddb..484ee4cbc 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,18 +19,15 @@ import java.io.IOException; import java.util.Arrays; -import org.junit.Test; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -41,136 +38,120 @@ * @author Andreas Evers * @author Andy Wilkinson */ -public class ResponseHeadersSnippetTests extends AbstractSnippetTests { - - public ResponseHeadersSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); +class ResponseHeadersSnippetTests { + + @RenderedSnippetTest + void responseWithHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"), + headerWithName("Content-Type").description("two"), headerWithName("Etag").description("three"), + headerWithName("Cache-Control").description("five"), headerWithName("Vary").description("six"))) + .document(operationBuilder.response() + .header("X-Test", "test") + .header("Content-Type", "application/json") + .header("Etag", "lskjadldj3ii32l2ij23") + .header("Cache-Control", "max-age=0") + .header("Vary", "User-Agent") + .build()); + assertThat(snippets.responseHeaders()).isTable((table) -> table.withHeader("Name", "Description") + .row("`X-Test`", "one") + .row("`Content-Type`", "two") + .row("`Etag`", "three") + .row("`Cache-Control`", "five") + .row("`Vary`", "six")); } - @Test - public void responseWithHeaders() throws IOException { - this.snippet.expectResponseHeaders().withContents( - tableWithHeader("Name", "Description").row("`X-Test`", "one") - .row("`Content-Type`", "two").row("`Etag`", "three") - .row("`Cache-Control`", "five").row("`Vary`", "six")); - new ResponseHeadersSnippet( - Arrays.asList(headerWithName("X-Test").description("one"), - headerWithName("Content-Type").description("two"), - headerWithName("Etag").description("three"), - headerWithName("Cache-Control").description("five"), - headerWithName("Vary").description("six"))) - .document( - this.operationBuilder.response() - .header("X-Test", "test") - .header("Content-Type", - "application/json") - .header("Etag", "lskjadldj3ii32l2ij23") - .header("Cache-Control", "max-age=0") - .header("Vary", "User-Agent").build()); + @RenderedSnippetTest + void caseInsensitiveResponseHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"))) + .document(operationBuilder.response().header("X-test", "test").build()); + assertThat(snippets.responseHeaders()) + .isTable((table) -> table.withHeader("Name", "Description").row("`X-Test`", "one")); } - @Test - public void caseInsensitiveResponseHeaders() throws IOException { - this.snippet.expectResponseHeaders().withContents( - tableWithHeader("Name", "Description").row("`X-Test`", "one")); - new ResponseHeadersSnippet( - Arrays.asList(headerWithName("X-Test").description("one"))) - .document(this.operationBuilder.response() - .header("X-test", "test").build()); + @RenderedSnippetTest + void undocumentedResponseHeader(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one"))) + .document(operationBuilder.response().header("X-Test", "test").header("Content-Type", "*/*").build()); + assertThat(snippets.responseHeaders()) + .isTable((table) -> table.withHeader("Name", "Description").row("`X-Test`", "one")); } - @Test - public void undocumentedResponseHeader() throws IOException { - this.snippet.expectResponseHeaders().withContents( - tableWithHeader("Name", "Description").row("`X-Test`", "one")); - new ResponseHeadersSnippet( - Arrays.asList(headerWithName("X-Test").description("one"))).document( - this.operationBuilder.response().header("X-Test", "test") - .header("Content-Type", "*/*").build()); + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-headers", template = "response-headers-with-title") + void responseHeadersWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description("one")), + attributes(key("title").value("Custom title"))) + .document(operationBuilder.response().header("X-Test", "test").build()); + assertThat(snippets.responseHeaders()).contains("Custom title"); } - @Test - public void responseHeadersWithCustomAttributes() throws IOException { - this.snippet.expectResponseHeaders().withContents(containsString("Custom title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-headers")) - .willReturn(snippetResource("response-headers-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-headers", template = "response-headers-with-extra-column") + void responseHeadersWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseHeadersSnippet( - Arrays.asList(headerWithName("X-Test").description("one")), - attributes( - key("title").value("Custom title"))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .response().header("X-Test", "test") - .build()); + Arrays.asList(headerWithName("X-Test").description("one").attributes(key("foo").value("alpha")), + headerWithName("Content-Type").description("two").attributes(key("foo").value("bravo")), + headerWithName("Etag").description("three").attributes(key("foo").value("charlie")))) + .document(operationBuilder.response() + .header("X-Test", "test") + .header("Content-Type", "application/json") + .header("Etag", "lskjadldj3ii32l2ij23") + .build()); + assertThat(snippets.responseHeaders()).isTable((table) -> table.withHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha") + .row("Content-Type", "two", "bravo") + .row("Etag", "three", "charlie")); } - @Test - public void responseHeadersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectResponseHeaders() - .withContents(tableWithHeader("Name", "Description", "Foo") - .row("X-Test", "one", "alpha").row("Content-Type", "two", "bravo") - .row("Etag", "three", "charlie")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-headers")) - .willReturn(snippetResource("response-headers-with-extra-column")); - new ResponseHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one") - .attributes(key("foo").value("alpha")), - headerWithName("Content-Type").description("two") - .attributes(key("foo").value("bravo")), - headerWithName("Etag").description("three") - .attributes(key("foo").value("charlie")))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .response().header("X-Test", "test") - .header("Content-Type", - "application/json") - .header("Etag", "lskjadldj3ii32l2ij23") - .build()); + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + HeaderDocumentation + .responseHeaders(headerWithName("X-Test").description("one"), + headerWithName("Content-Type").description("two"), headerWithName("Etag").description("three")) + .and(headerWithName("Cache-Control").description("five"), headerWithName("Vary").description("six")) + .document(operationBuilder.response() + .header("X-Test", "test") + .header("Content-Type", "application/json") + .header("Etag", "lskjadldj3ii32l2ij23") + .header("Cache-Control", "max-age=0") + .header("Vary", "User-Agent") + .build()); + assertThat(snippets.responseHeaders()).isTable((table) -> table.withHeader("Name", "Description") + .row("`X-Test`", "one") + .row("`Content-Type`", "two") + .row("`Etag`", "three") + .row("`Cache-Control`", "five") + .row("`Vary`", "six")); } - @Test - public void additionalDescriptors() throws IOException { - this.snippet.expectResponseHeaders().withContents( - tableWithHeader("Name", "Description").row("`X-Test`", "one") - .row("`Content-Type`", "two").row("`Etag`", "three") - .row("`Cache-Control`", "five").row("`Vary`", "six")); - HeaderDocumentation - .responseHeaders(headerWithName("X-Test").description("one"), - headerWithName("Content-Type").description("two"), - headerWithName("Etag").description("three")) - .and(headerWithName("Cache-Control").description("five"), - headerWithName("Vary").description("six")) - .document(this.operationBuilder.response().header("X-Test", "test") - .header("Content-Type", "application/json") - .header("Etag", "lskjadldj3ii32l2ij23") - .header("Cache-Control", "max-age=0").header("Vary", "User-Agent") - .build()); + @RenderedSnippetTest + void tableCellContentIsEscapedWhenNecessary(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseHeadersSnippet(Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) + .document(operationBuilder.response().header("Foo|Bar", "baz").build()); + assertThat(snippets.responseHeaders()) + .isTable((table) -> table.withHeader("Name", "Description").row("`Foo|Bar`", "one|two")); } - @Test - public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectResponseHeaders().withContents( - tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), - escapeIfNecessary("one|two"))); - new ResponseHeadersSnippet( - Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) - .document(this.operationBuilder.response() - .header("Foo|Bar", "baz").build()); + @SnippetTest + void missingResponseHeader(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy( + () -> new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type").description("one"))) + .document(operationBuilder.response().build())) + .withMessage("Headers with the following names were not found" + " in the response: [Content-Type]"); } - private String escapeIfNecessary(String input) { - if (this.templateFormat.equals(TemplateFormats.markdown())) { - return input; - } - return input.replace("|", "\\|"); + @SnippetTest + void undocumentedResponseHeaderAndMissingResponseHeader(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy( + () -> new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type").description("one"))) + .document(operationBuilder.response().header("X-Test", "test").build())) + .withMessageEndingWith("Headers with the following names were not found in the response: [Content-Type]"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 557d2c403..a37e3fd3c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,14 @@ import java.io.IOException; -import org.junit.Test; - import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -41,328 +35,213 @@ * @author Andy Wilkinson * @author Jonathan Pearlin */ -public class HttpRequestSnippetTests extends AbstractSnippetTests { +class HttpRequestSnippetTests { private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; - public HttpRequestSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } - - @Test - public void getRequest() throws IOException { - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") - .header(HttpHeaders.HOST, "localhost")); - - new HttpRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").header("Alpha", "a").build()); - } - - @Test - public void getRequestWithParameters() throws IOException { - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.GET, "/foo?b=bravo") - .header("Alpha", "a").header(HttpHeaders.HOST, "localhost")); - + @RenderedSnippetTest + void getRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .header("Alpha", "a").param("b", "bravo").build()); - } - - @Test - public void getRequestWithPort() throws IOException { - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") - .header(HttpHeaders.HOST, "localhost:8080")); - - new HttpRequestSnippet().document(this.operationBuilder - .request("http://localhost:8080/foo").header("Alpha", "a").build()); - } - - @Test - public void getRequestWithQueryString() throws IOException { - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.GET, "/foo?bar=baz") - .header(HttpHeaders.HOST, "localhost")); - - new HttpRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo?bar=baz").build()); + .document(operationBuilder.request("http://localhost/foo").header("Alpha", "a").build()); + assertThat(snippets.httpRequest()) + .isHttpRequest((request) -> request.get("/foo").header("Alpha", "a").header(HttpHeaders.HOST, "localhost")); } - @Test - public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.GET, "/foo?bar") - .header(HttpHeaders.HOST, "localhost")); - - new HttpRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo?bar").build()); - } - - @Test - public void getWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") - .header(HttpHeaders.HOST, "localhost")); - + @RenderedSnippetTest + void getRequestWithQueryParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo?a=alpha") - .param("a", "alpha").param("b", "bravo").build()); + .document(operationBuilder.request("http://localhost/foo?b=bravo").header("Alpha", "a").build()); + assertThat(snippets.httpRequest()).isHttpRequest( + (request) -> request.get("/foo?b=bravo").header("Alpha", "a").header(HttpHeaders.HOST, "localhost")); } - @Test - public void getWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") - .header(HttpHeaders.HOST, "localhost")); - - new HttpRequestSnippet().document( - this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") - .param("a", "alpha").param("b", "bravo").build()); + @RenderedSnippetTest + void getRequestWithPort(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet() + .document(operationBuilder.request("http://localhost:8080/foo").header("Alpha", "a").build()); + assertThat(snippets.httpRequest()).isHttpRequest( + (request) -> request.get("/foo").header("Alpha", "a").header(HttpHeaders.HOST, "localhost:8080")); } - @Test - public void postRequestWithContent() throws IOException { - String content = "Hello, world"; - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.POST, "/foo") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - - new HttpRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").method("POST").content(content).build()); + @RenderedSnippetTest + void getRequestWithCookies(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/foo") + .cookie("name1", "value1") + .cookie("name2", "value2") + .build()); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.get("/foo") + .header(HttpHeaders.HOST, "localhost") + .header(HttpHeaders.COOKIE, "name1=value1; name2=value2")); } - @Test - public void postRequestWithContentAndParameters() throws IOException { - String content = "Hello, world"; - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.POST, "/foo?a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - - new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .method("POST").param("a", "alpha").content(content).build()); + @RenderedSnippetTest + void getRequestWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/foo?bar=baz").build()); + assertThat(snippets.httpRequest()) + .isHttpRequest((request) -> request.get("/foo?bar=baz").header(HttpHeaders.HOST, "localhost")); } - @Test - public void postRequestWithContentAndDisjointQueryStringAndParameters() + @RenderedSnippetTest + void getRequestWithQueryStringWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { - String content = "Hello, world"; - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - - new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo?b=bravo") - .method("POST").param("a", "alpha").content(content).build()); + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/foo?bar").build()); + assertThat(snippets.httpRequest()) + .isHttpRequest((request) -> request.get("/foo?bar").header(HttpHeaders.HOST, "localhost")); } - @Test - public void postRequestWithContentAndPartiallyOverlappingQueryStringAndParameters() - throws IOException { + @RenderedSnippetTest + void postRequestWithContent(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - - new HttpRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo?b=bravo").method("POST") - .param("a", "alpha").param("b", "bravo").content(content).build()); + new HttpRequestSnippet() + .document(operationBuilder.request("http://localhost/foo").method("POST").content(content).build()); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.post("/foo") + .header(HttpHeaders.HOST, "localhost") + .content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); } - @Test - public void postRequestWithContentAndTotallyOverlappingQueryStringAndParameters() + @RenderedSnippetTest + void postRequestWithContentAndQueryParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + new HttpRequestSnippet() + .document(operationBuilder.request("http://localhost/foo?a=alpha").method("POST").content(content).build()); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.post("/foo?a=alpha") + .header(HttpHeaders.HOST, "localhost") + .content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo?b=bravo&a=alpha").method("POST") - .param("a", "alpha").param("b", "bravo").content(content).build()); } - @Test - public void postRequestWithCharset() throws IOException { + @RenderedSnippetTest + void postRequestWithCharset(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.POST, "/foo") - .header("Content-Type", "text/plain;charset=UTF-8") - .header(HttpHeaders.HOST, "localhost") - .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) - .content(japaneseContent)); - - new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .method("POST").header("Content-Type", "text/plain;charset=UTF-8") - .content(contentBytes).build()); + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/foo") + .method("POST") + .header("Content-Type", "text/plain;charset=UTF-8") + .content(contentBytes) + .build()); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.post("/foo") + .header("Content-Type", "text/plain;charset=UTF-8") + .header(HttpHeaders.HOST, "localhost") + .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) + .content(japaneseContent)); } - @Test - public void postRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.POST, "/foo") - .header(HttpHeaders.HOST, "localhost") - .header("Content-Type", "application/x-www-form-urlencoded") - .content("b%26r=baz&a=alpha")); - + @RenderedSnippetTest + void putRequestWithContent(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + String content = "Hello, world"; new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .method("POST").param("b&r", "baz").param("a", "alpha").build()); + .document(operationBuilder.request("http://localhost/foo").method("PUT").content(content).build()); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.put("/foo") + .header(HttpHeaders.HOST, "localhost") + .content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); } - @Test - public void postRequestWithParameterWithNoValue() throws IOException { - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.POST, "/foo") - .header(HttpHeaders.HOST, "localhost") - .header("Content-Type", "application/x-www-form-urlencoded") - .content("bar=")); - - new HttpRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").method("POST").param("bar").build()); + @RenderedSnippetTest + void multipartPost(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", "<< data >>".getBytes()) + .build()); + String expectedContent = createPart( + String.format("Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.post("/upload") + .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) + .header(HttpHeaders.HOST, "localhost") + .content(expectedContent)); } - @Test - public void putRequestWithContent() throws IOException { - String content = "Hello, world"; - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.PUT, "/foo") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - - new HttpRequestSnippet().document(this.operationBuilder - .request("http://localhost/foo").method("PUT").content(content).build()); + @RenderedSnippetTest + void multipartPut(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/upload") + .method("PUT") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", "<< data >>".getBytes()) + .build()); + String expectedContent = createPart( + String.format("Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.put("/upload") + .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) + .header(HttpHeaders.HOST, "localhost") + .content(expectedContent)); } - @Test - public void putRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.PUT, "/foo") - .header(HttpHeaders.HOST, "localhost") - .header("Content-Type", "application/x-www-form-urlencoded") - .content("b%26r=baz&a=alpha")); - - new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .method("PUT").param("b&r", "baz").param("a", "alpha").build()); + @RenderedSnippetTest + void multipartPatch(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/upload") + .method("PATCH") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", "<< data >>".getBytes()) + .build()); + String expectedContent = createPart( + String.format("Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.patch("/upload") + .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) + .header(HttpHeaders.HOST, "localhost") + .content(expectedContent)); } - @Test - public void multipartPost() throws IOException { - String expectedContent = createPart(String.format( - "Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.POST, "/upload") - .header("Content-Type", - "multipart/form-data; boundary=" + BOUNDARY) - .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document( - this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", "<< data >>".getBytes()).build()); + @RenderedSnippetTest + void multipartPostWithFilename(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", "<< data >>".getBytes()) + .submittedFileName("image.png") + .build()); + String expectedContent = createPart(String + .format("Content-Disposition: " + "form-data; " + "name=image; filename=image.png%n%n<< data >>")); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.post("/upload") + .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) + .header(HttpHeaders.HOST, "localhost") + .content(expectedContent)); } - @Test - public void multipartPostWithParameters() throws IOException { - String param1Part = createPart( - String.format("Content-Disposition: form-data; " + "name=a%n%napple"), - false); - String param2Part = createPart( - String.format("Content-Disposition: form-data; " + "name=a%n%navocado"), - false); - String param3Part = createPart( - String.format("Content-Disposition: form-data; " + "name=b%n%nbanana"), - false); - String filePart = createPart(String - .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); - String expectedContent = param1Part + param2Part + param3Part + filePart; - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.POST, "/upload") - .header("Content-Type", - "multipart/form-data; boundary=" + BOUNDARY) - .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document( - this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .param("a", "apple", "avocado").param("b", "banana") - .part("image", "<< data >>".getBytes()).build()); + @RenderedSnippetTest + void multipartPostWithContentType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpRequestSnippet().document(operationBuilder.request("http://localhost/upload") + .method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", "<< data >>".getBytes()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .build()); + String expectedContent = createPart(String + .format("Content-Disposition: form-data; name=image%nContent-Type: " + "image/png%n%n<< data >>")); + assertThat(snippets.httpRequest()).isHttpRequest((request) -> request.post("/upload") + .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) + .header(HttpHeaders.HOST, "localhost") + .content(expectedContent)); } - @Test - public void multipartPostWithParameterWithNoValue() throws IOException { - String paramPart = createPart( - String.format("Content-Disposition: form-data; " + "name=a%n"), false); - String filePart = createPart(String - .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); - String expectedContent = paramPart + filePart; - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.POST, "/upload") - .header("Content-Type", - "multipart/form-data; boundary=" + BOUNDARY) - .header(HttpHeaders.HOST, "localhost").content(expectedContent)); + @RenderedSnippetTest + void getRequestWithCustomHost(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new HttpRequestSnippet().document( - this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .param("a").part("image", "<< data >>".getBytes()).build()); + operationBuilder.request("http://localhost/foo").header(HttpHeaders.HOST, "api.example.com").build()); + assertThat(snippets.httpRequest()) + .isHttpRequest((request) -> request.get("/foo").header(HttpHeaders.HOST, "api.example.com")); } - @Test - public void multipartPostWithContentType() throws IOException { - String expectedContent = createPart( - String.format("Content-Disposition: form-data; name=image%nContent-Type: " - + "image/png%n%n<< data >>")); - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.POST, "/upload") - .header("Content-Type", - "multipart/form-data; boundary=" + BOUNDARY) - .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document( - this.operationBuilder.request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", "<< data >>".getBytes()) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .build()); + @RenderedSnippetTest + @SnippetTemplate(snippet = "http-request", template = "http-request-with-title") + void requestWithCustomSnippetAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new HttpRequestSnippet(attributes(key("title").value("Title for the request"))) + .document(operationBuilder.request("http://localhost/foo").build()); + assertThat(snippets.httpRequest()).contains("Title for the request"); } - @Test - public void getRequestWithCustomHost() throws IOException { - this.snippet.expectHttpRequest() - .withContents(httpRequest(RequestMethod.GET, "/foo") - .header(HttpHeaders.HOST, "api.example.com")); + @RenderedSnippetTest + void deleteWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new HttpRequestSnippet() - .document(this.operationBuilder.request("http://localhost/foo") - .header(HttpHeaders.HOST, "api.example.com").build()); - } - - @Test - public void requestWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpRequest() - .withContents(containsString("Title for the request")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("http-request")) - .willReturn(snippetResource("http-request-with-title")); - new HttpRequestSnippet( - attributes( - key("title").value("Title for the request"))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost/foo").build()); + .document(operationBuilder.request("http://localhost/foo?a=alpha&b=bravo").method("DELETE").build()); + assertThat(snippets.httpRequest()) + .isHttpRequest((request) -> request.delete("/foo?a=alpha&b=bravo").header("Host", "localhost")); } private String createPart(String content) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 16875dab2..0ce5785ea 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,16 @@ import java.io.IOException; -import org.junit.Test; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -41,71 +37,66 @@ * @author Andy Wilkinson * @author Jonathan Pearlin */ -public class HttpResponseSnippetTests extends AbstractSnippetTests { - - public HttpResponseSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class HttpResponseSnippetTests { - @Test - public void basicResponse() throws IOException { - this.snippet.expectHttpResponse().withContents(httpResponse(HttpStatus.OK)); - new HttpResponseSnippet().document(this.operationBuilder.build()); + @RenderedSnippetTest + void basicResponse(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpResponseSnippet().document(operationBuilder.build()); + assertThat(snippets.httpResponse()).isHttpResponse((response) -> response.ok()); } - @Test - public void nonOkResponse() throws IOException { - this.snippet.expectHttpResponse() - .withContents(httpResponse(HttpStatus.BAD_REQUEST)); - new HttpResponseSnippet().document(this.operationBuilder.response() - .status(HttpStatus.BAD_REQUEST.value()).build()); + @RenderedSnippetTest + void nonOkResponse(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpResponseSnippet().document(operationBuilder.response().status(HttpStatus.BAD_REQUEST).build()); + assertThat(snippets.httpResponse()).isHttpResponse((response) -> response.badRequest()); } - @Test - public void responseWithHeaders() throws IOException { - this.snippet.expectHttpResponse().withContents(httpResponse(HttpStatus.OK) - .header("Content-Type", "application/json").header("a", "alpha")); - new HttpResponseSnippet() - .document(this.operationBuilder.response() - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_JSON_VALUE) - .header("a", "alpha").build()); + @RenderedSnippetTest + void responseWithHeaders(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpResponseSnippet().document(operationBuilder.response() + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha") + .build()); + assertThat(snippets.httpResponse()).isHttpResponse( + (response) -> response.ok().header("Content-Type", "application/json").header("a", "alpha")); } - @Test - public void responseWithContent() throws IOException { + @RenderedSnippetTest + void responseWithContent(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { String content = "content"; - this.snippet.expectHttpResponse() - .withContents(httpResponse(HttpStatus.OK).content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpResponseSnippet() - .document(this.operationBuilder.response().content(content).build()); + new HttpResponseSnippet().document(operationBuilder.response().content(content).build()); + assertThat(snippets.httpResponse()).isHttpResponse((response) -> response.ok() + .content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); } - @Test - public void responseWithCharset() throws IOException { + @RenderedSnippetTest + void responseWithCharset(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpResponse() - .withContents(httpResponse(HttpStatus.OK) - .header("Content-Type", "text/plain;charset=UTF-8") - .content(japaneseContent) - .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)); - new HttpResponseSnippet().document(this.operationBuilder.response() - .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) - .build()); + new HttpResponseSnippet().document(operationBuilder.response() + .header("Content-Type", "text/plain;charset=UTF-8") + .content(contentBytes) + .build()); + assertThat(snippets.httpResponse()).isHttpResponse((response) -> response.ok() + .header("Content-Type", "text/plain;charset=UTF-8") + .content(japaneseContent) + .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)); } - @Test - public void responseWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpResponse() - .withContents(containsString("Title for the response")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("http-response")) - .willReturn(snippetResource("http-response-with-title")); + @RenderedSnippetTest + @SnippetTemplate(snippet = "http-response", template = "http-response-with-title") + void responseWithCustomSnippetAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new HttpResponseSnippet(attributes(key("title").value("Title for the response"))) - .document(this.operationBuilder.attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + .document(operationBuilder.build()); + assertThat(snippets.httpResponse()).contains("Title for the response"); + } + + @RenderedSnippetTest + void responseWithCustomStatus(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new HttpResponseSnippet().document(operationBuilder.response().status(HttpStatusCode.valueOf(215)).build()); + assertThat(snippets.httpResponse()).isHttpResponse(((response) -> response.status(215))); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java index 67d4eba97..eb5875f8b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,10 @@ import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -30,6 +29,8 @@ import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -38,43 +39,58 @@ * * @author Andy Wilkinson */ -public class ContentTypeLinkExtractorTests { +class ContentTypeLinkExtractorTests { private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - @Rule - public ExpectedException thrown = ExpectedException.none(); + private final String halBody = "{ \"_links\" : { \"someRel\" : { \"href\" : \"someHref\" }} }"; @Test - public void extractionFailsWithNullContentType() throws IOException { - this.thrown.expect(IllegalStateException.class); - new ContentTypeLinkExtractor().extractLinks( - this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), null)); + void extractionFailsWithNullContentType() { + assertThatIllegalStateException().isThrownBy(() -> new ContentTypeLinkExtractor() + .extractLinks(this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), null))); } @Test - public void extractorCalledWithMatchingContextType() throws IOException { + void extractorCalledWithMatchingContextType() throws IOException { Map extractors = new HashMap<>(); LinkExtractor extractor = mock(LinkExtractor.class); extractors.put(MediaType.APPLICATION_JSON, extractor); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); - OperationResponse response = this.responseFactory.create(HttpStatus.OK, - httpHeaders, null); + OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, null); new ContentTypeLinkExtractor(extractors).extractLinks(response); verify(extractor).extractLinks(response); } @Test - public void extractorCalledWithCompatibleContextType() throws IOException { + void extractorCalledWithCompatibleContextType() throws IOException { Map extractors = new HashMap<>(); LinkExtractor extractor = mock(LinkExtractor.class); extractors.put(MediaType.APPLICATION_JSON, extractor); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.parseMediaType("application/json;foo=bar")); - OperationResponse response = this.responseFactory.create(HttpStatus.OK, - httpHeaders, null); + OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, null); new ContentTypeLinkExtractor(extractors).extractLinks(response); verify(extractor).extractLinks(response); } + + @Test + void extractsLinksFromVndHalMediaType() throws IOException { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.parseMediaType("application/vnd.hal+json")); + OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, this.halBody.getBytes()); + Map> links = new ContentTypeLinkExtractor().extractLinks(response); + assertThat(links).containsKey("someRel"); + } + + @Test + void extractsLinksFromHalFormsMediaType() throws IOException { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.parseMediaType("application/prs.hal-forms+json")); + OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, this.halBody.getBytes()); + Map> links = new ContentTypeLinkExtractor().extractLinks(response); + assertThat(links).containsKey("someRel"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index db6aa7256..5346d512c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,10 +24,9 @@ import java.util.List; import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.http.HttpStatus; import org.springframework.restdocs.operation.OperationResponse; @@ -36,16 +35,16 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; /** - * Parameterized tests for {@link HalLinkExtractor} and {@link AtomLinkExtractor} with - * various payloads. + * Tests for {@link HalLinkExtractor} and {@link AtomLinkExtractor} with various payloads. * * @author Andy Wilkinson */ -@RunWith(Parameterized.class) -public class LinkExtractorsPayloadTests { +@ParameterizedClass(name = "{1}") +@MethodSource("parameters") +class LinkExtractorsPayloadTests { private final OperationResponseFactory responseFactory = new OperationResponseFactory(); @@ -53,63 +52,55 @@ public class LinkExtractorsPayloadTests { private final String linkType; - @Parameters(name = "{1}") - public static Collection data() { + static Collection parameters() { return Arrays.asList(new Object[] { new HalLinkExtractor(), "hal" }, new Object[] { new AtomLinkExtractor(), "atom" }); } - public LinkExtractorsPayloadTests(LinkExtractor linkExtractor, String linkType) { + LinkExtractorsPayloadTests(LinkExtractor linkExtractor, String linkType) { this.linkExtractor = linkExtractor; this.linkType = linkType; } @Test - public void singleLink() throws IOException { - Map> links = this.linkExtractor - .extractLinks(createResponse("single-link")); - assertLinks(Arrays.asList(new Link("alpha", "https://alpha.example.com", "Alpha")), - links); + void singleLink() throws IOException { + Map> links = this.linkExtractor.extractLinks(createResponse("single-link")); + assertLinks(Arrays.asList(new Link("alpha", "https://alpha.example.com", "Alpha")), links); } @Test - public void multipleLinksWithDifferentRels() throws IOException { + void multipleLinksWithDifferentRels() throws IOException { Map> links = this.linkExtractor - .extractLinks(createResponse("multiple-links-different-rels")); + .extractLinks(createResponse("multiple-links-different-rels")); assertLinks(Arrays.asList(new Link("alpha", "https://alpha.example.com", "Alpha"), new Link("bravo", "https://bravo.example.com")), links); } @Test - public void multipleLinksWithSameRels() throws IOException { - Map> links = this.linkExtractor - .extractLinks(createResponse("multiple-links-same-rels")); - assertLinks(Arrays.asList( - new Link("alpha", "https://alpha.example.com/one", "Alpha one"), + void multipleLinksWithSameRels() throws IOException { + Map> links = this.linkExtractor.extractLinks(createResponse("multiple-links-same-rels")); + assertLinks(Arrays.asList(new Link("alpha", "https://alpha.example.com/one", "Alpha one"), new Link("alpha", "https://alpha.example.com/two")), links); } @Test - public void noLinks() throws IOException { - Map> links = this.linkExtractor - .extractLinks(createResponse("no-links")); + void noLinks() throws IOException { + Map> links = this.linkExtractor.extractLinks(createResponse("no-links")); assertLinks(Collections.emptyList(), links); } @Test - public void linksInTheWrongFormat() throws IOException { - Map> links = this.linkExtractor - .extractLinks(createResponse("wrong-format")); + void linksInTheWrongFormat() throws IOException { + Map> links = this.linkExtractor.extractLinks(createResponse("wrong-format")); assertLinks(Collections.emptyList(), links); } - private void assertLinks(List expectedLinks, - Map> actualLinks) { + private void assertLinks(List expectedLinks, Map> actualLinks) { MultiValueMap expectedLinksByRel = new LinkedMultiValueMap<>(); for (Link expectedLink : expectedLinks) { expectedLinksByRel.add(expectedLink.getRel(), expectedLink); } - assertEquals(expectedLinksByRel, actualLinks); + assertThat(actualLinks).isEqualTo(expectedLinksByRel); } private OperationResponse createResponse(String contentName) throws IOException { @@ -118,7 +109,7 @@ private OperationResponse createResponse(String contentName) throws IOException } private File getPayloadFile(String name) { - return new File("src/test/resources/link-payloads/" + this.linkType + "/" + name - + ".json"); + return new File("src/test/resources/link-payloads/" + this.linkType + "/" + name + ".json"); } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java deleted file mode 100644 index 7d66feca3..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.hypermedia; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; - -/** - * Tests for failures when rendering {@link LinksSnippet} due to missing or undocumented - * links. - * - * @author Andy Wilkinson - */ -public class LinksSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void undocumentedLink() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo( - "Links with the following relations were not" + " documented: [foo]")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), - Collections.emptyList()) - .document(this.operationBuilder.build()); - } - - @Test - public void missingLink() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Links with the following relations were not" - + " found in the response: [foo]")); - new LinksSnippet(new StubLinkExtractor(), - Arrays.asList(new LinkDescriptor("foo").description("bar"))) - .document(this.operationBuilder.build()); - } - - @Test - public void undocumentedLinkAndMissingLink() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Links with the following relations were not" - + " documented: [a]. Links with the following relations were not" - + " found in the response: [foo]")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), - Arrays.asList(new LinkDescriptor("foo").description("bar"))) - .document(this.operationBuilder.build()); - } - - @Test - public void linkWithNoDescription() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage( - equalTo("No description was provided for the link with rel 'foo' and no" - + " title was available from the link in the payload")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), - Arrays.asList(new LinkDescriptor("foo"))) - .document(this.operationBuilder.build()); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index feca03ca5..7782ce42f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,17 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; -import org.junit.Test; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -39,145 +37,145 @@ * * @author Andy Wilkinson */ -public class LinksSnippetTests extends AbstractSnippetTests { - - public LinksSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } - - @Test - public void ignoredLink() throws IOException { - this.snippet.expectLinks().withContents( - tableWithHeader("Relation", "Description").row("`b`", "Link b")); - new LinksSnippet( - new StubLinkExtractor().withLinks(new Link("a", "alpha"), - new Link("b", "bravo")), - Arrays.asList(new LinkDescriptor("a").ignored(), - new LinkDescriptor("b").description("Link b"))) - .document(this.operationBuilder.build()); +class LinksSnippetTests { + + @RenderedSnippetTest + void ignoredLink(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), + Arrays.asList(new LinkDescriptor("a").ignored(), new LinkDescriptor("b").description("Link b"))) + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`b`", "Link b")); } - @Test - public void allUndocumentedLinksCanBeIgnored() throws IOException { - this.snippet.expectLinks().withContents( - tableWithHeader("Relation", "Description").row("`b`", "Link b")); - new LinksSnippet( - new StubLinkExtractor().withLinks(new Link("a", "alpha"), - new Link("b", "bravo")), + @RenderedSnippetTest + void allUndocumentedLinksCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("b").description("Link b")), true) - .document(this.operationBuilder.build()); + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`b`", "Link b")); } - @Test - public void presentOptionalLink() throws IOException { - this.snippet.expectLinks().withContents( - tableWithHeader("Relation", "Description").row("`foo`", "bar")); + @RenderedSnippetTest + void presentOptionalLink(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(this.operationBuilder.build()); + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`foo`", "bar")); } - @Test - public void missingOptionalLink() throws IOException { - this.snippet.expectLinks().withContents( - tableWithHeader("Relation", "Description").row("`foo`", "bar")); + @RenderedSnippetTest + void missingOptionalLink(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(this.operationBuilder.build()); + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`foo`", "bar")); } - @Test - public void documentedLinks() throws IOException { - this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") - .row("`a`", "one").row("`b`", "two")); - new LinksSnippet( - new StubLinkExtractor().withLinks(new Link("a", "alpha"), - new Link("b", "bravo")), - Arrays.asList(new LinkDescriptor("a").description("one"), - new LinkDescriptor("b").description("two"))) - .document(this.operationBuilder.build()); + @RenderedSnippetTest + void documentedLinks(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), + Arrays.asList(new LinkDescriptor("a").description("one"), new LinkDescriptor("b").description("two"))) + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void linkDescriptionFromTitleInPayload() throws IOException { - this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") - .row("`a`", "one").row("`b`", "Link b")); + @RenderedSnippetTest + void linkDescriptionFromTitleInPayload(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new LinksSnippet( - new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"), - new Link("b", "bravo", "Link b")), - Arrays.asList(new LinkDescriptor("a").description("one"), - new LinkDescriptor("b"))).document(this.operationBuilder.build()); + new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"), new Link("b", "bravo", "Link b")), + Arrays.asList(new LinkDescriptor("a").description("one"), new LinkDescriptor("b"))) + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`a`", "one").row("`b`", "Link b")); } - @Test - public void linksWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("links")) - .willReturn(snippetResource("links-with-title")); - this.snippet.expectLinks().withContents(containsString("Title for the links")); - - new LinksSnippet( - new StubLinkExtractor().withLinks(new Link("a", "alpha"), - new Link("b", "bravo")), - Arrays.asList(new LinkDescriptor("a").description("one"), - new LinkDescriptor("b").description("two")), + @RenderedSnippetTest + @SnippetTemplate(snippet = "links", template = "links-with-title") + void linksWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), + Arrays.asList(new LinkDescriptor("a").description("one"), new LinkDescriptor("b").description("two")), attributes(key("title").value("Title for the links"))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .build()); + .document(operationBuilder.build()); + assertThat(snippets.links()).contains("Title for the links"); } - @Test - public void linksWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("links")) - .willReturn(snippetResource("links-with-extra-column")); - this.snippet.expectLinks() - .withContents(tableWithHeader("Relation", "Description", "Foo") - .row("a", "one", "alpha").row("b", "two", "bravo")); - - new LinksSnippet( - new StubLinkExtractor().withLinks(new Link("a", "alpha"), - new Link("b", "bravo")), - Arrays.asList( - new LinkDescriptor("a").description("one") - .attributes(key("foo").value("alpha")), - new LinkDescriptor("b").description("two") - .attributes(key("foo").value("bravo")))) - .document(this.operationBuilder.attribute( - TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .build()); + @RenderedSnippetTest + @SnippetTemplate(snippet = "links", template = "links-with-extra-column") + void linksWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), + Arrays.asList(new LinkDescriptor("a").description("one").attributes(key("foo").value("alpha")), + new LinkDescriptor("b").description("two").attributes(key("foo").value("bravo")))) + .document(operationBuilder.build()); + assertThat(snippets.links()).isTable((table) -> table.withHeader("Relation", "Description", "Foo") + .row("a", "one", "alpha") + .row("b", "two", "bravo")); } - @Test - public void additionalDescriptors() throws IOException { - this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") - .row("`a`", "one").row("`b`", "two")); + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { HypermediaDocumentation - .links(new StubLinkExtractor().withLinks(new Link("a", "alpha"), - new Link("b", "bravo")), - new LinkDescriptor("a").description("one")) - .and(new LinkDescriptor("b").description("two")) - .document(this.operationBuilder.build()); + .links(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), + new LinkDescriptor("a").description("one")) + .and(new LinkDescriptor("b").description("two")) + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") - .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + @RenderedSnippetTest + void tableCellContentIsEscapedWhenNecessary(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new LinksSnippet(new StubLinkExtractor().withLinks(new Link("Foo|Bar", "foo")), Arrays.asList(new LinkDescriptor("Foo|Bar").description("one|two"))) - .document(this.operationBuilder.build()); + .document(operationBuilder.build()); + assertThat(snippets.links()) + .isTable((table) -> table.withHeader("Relation", "Description").row("`Foo|Bar`", "one|two")); + } + + @SnippetTest + void undocumentedLink(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), + Collections.emptyList()) + .document(operationBuilder.build())) + .withMessage("Links with the following relations were not documented: [foo]"); + } + + @SnippetTest + void missingLink(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new LinksSnippet(new StubLinkExtractor(), + Arrays.asList(new LinkDescriptor("foo").description("bar"))) + .document(operationBuilder.build())) + .withMessage("Links with the following relations were not found in the response: [foo]"); + } + + @SnippetTest + void undocumentedLinkAndMissingLink(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), + Arrays.asList(new LinkDescriptor("foo").description("bar"))) + .document(operationBuilder.build())) + .withMessage("Links with the following relations were not documented: [a]. Links with the following" + + " relations were not found in the response: [foo]"); } - private String escapeIfNecessary(String input) { - if (this.templateFormat.equals(TemplateFormats.markdown())) { - return input; - } - return input.replace("|", "\\|"); + @SnippetTest + void linkWithNoDescription(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), + Arrays.asList(new LinkDescriptor("foo"))) + .document(operationBuilder.build())) + .withMessage("No description was provided for the link with rel 'foo' and no title was available" + + " from the link in the payload"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/StubLinkExtractor.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/StubLinkExtractor.java index 71d7a4ee8..ca5e3d6b8 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/StubLinkExtractor.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/StubLinkExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,8 +32,7 @@ class StubLinkExtractor implements LinkExtractor { private MultiValueMap linksByRel = new LinkedMultiValueMap<>(); @Override - public MultiValueMap extractLinks(OperationResponse response) - throws IOException { + public MultiValueMap extractLinks(OperationResponse response) throws IOException { return this.linksByRel; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java deleted file mode 100644 index 8c7aa8439..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.operation; - -import org.junit.Test; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -/** - * Tests for {@link Parameters}. - * - * @author Andy Wilkinson - */ -public class ParametersTests { - - private final Parameters parameters = new Parameters(); - - @Test - public void queryStringForNoParameters() { - assertThat(this.parameters.toQueryString(), is(equalTo(""))); - } - - @Test - public void queryStringForSingleParameter() { - this.parameters.add("a", "b"); - assertThat(this.parameters.toQueryString(), is(equalTo("a=b"))); - } - - @Test - public void queryStringForSingleParameterWithMultipleValues() { - this.parameters.add("a", "b"); - this.parameters.add("a", "c"); - assertThat(this.parameters.toQueryString(), is(equalTo("a=b&a=c"))); - } - - @Test - public void queryStringForMutipleParameters() { - this.parameters.add("a", "alpha"); - this.parameters.add("b", "bravo"); - assertThat(this.parameters.toQueryString(), is(equalTo("a=alpha&b=bravo"))); - } - - @Test - public void queryStringForParameterWithEmptyValue() { - this.parameters.add("a", ""); - assertThat(this.parameters.toQueryString(), is(equalTo("a="))); - } - - @Test - public void queryStringForParameterWithNullValue() { - this.parameters.add("a", null); - assertThat(this.parameters.toQueryString(), is(equalTo("a="))); - } - - @Test - public void queryStringForParameterThatRequiresEncoding() { - this.parameters.add("a", "alpha&bravo"); - assertThat(this.parameters.toQueryString(), is(equalTo("a=alpha%26bravo"))); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java deleted file mode 100644 index d385714ec..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.operation; - -import java.net.URI; -import java.util.Arrays; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.hasEntry; -import static org.junit.Assert.assertThat; - -/** - * Tests for {@link QueryStringParser}. - * - * @author Andy Wilkinson - */ -public class QueryStringParserTests { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - private final QueryStringParser queryStringParser = new QueryStringParser(); - - @Test - public void noParameters() { - Parameters parameters = this.queryStringParser - .parse(URI.create("http://localhost")); - assertThat(parameters.size(), is(equalTo(0))); - } - - @Test - public void singleParameter() { - Parameters parameters = this.queryStringParser - .parse(URI.create("http://localhost?a=alpha")); - assertThat(parameters.size(), is(equalTo(1))); - assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); - } - - @Test - public void multipleParameters() { - Parameters parameters = this.queryStringParser - .parse(URI.create("http://localhost?a=alpha&b=bravo&c=charlie")); - assertThat(parameters.size(), is(equalTo(3))); - assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); - assertThat(parameters, hasEntry("b", Arrays.asList("bravo"))); - assertThat(parameters, hasEntry("c", Arrays.asList("charlie"))); - } - - @Test - public void multipleParametersWithSameKey() { - Parameters parameters = this.queryStringParser - .parse(URI.create("http://localhost?a=apple&a=avocado")); - assertThat(parameters.size(), is(equalTo(1))); - assertThat(parameters, hasEntry("a", Arrays.asList("apple", "avocado"))); - } - - @Test - public void encoded() { - Parameters parameters = this.queryStringParser - .parse(URI.create("http://localhost?a=al%26%3Dpha")); - assertThat(parameters.size(), is(equalTo(1))); - assertThat(parameters, hasEntry("a", Arrays.asList("al&=pha"))); - } - - @Test - public void malformedParameter() { - this.thrown.expect(IllegalArgumentException.class); - this.thrown - .expectMessage(equalTo("The parameter 'a=apple=avocado' is malformed")); - this.queryStringParser.parse(URI.create("http://localhost?a=apple=avocado")); - } -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java index da2110723..107a4d0d4 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.net.URI; import java.util.Collections; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -30,11 +30,8 @@ import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.Parameters; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link ContentModifyingOperationPreprocessor}. @@ -42,7 +39,7 @@ * @author Andy Wilkinson * */ -public class ContentModifyingOperationPreprocessorTests { +class ContentModifyingOperationPreprocessorTests { private final OperationRequestFactory requestFactory = new OperationRequestFactory(); @@ -59,33 +56,29 @@ public byte[] modifyContent(byte[] originalContent, MediaType mediaType) { }); @Test - public void modifyRequestContent() { - OperationRequest request = this.requestFactory.create( - URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), - new HttpHeaders(), new Parameters(), - Collections.emptyList()); + void modifyRequestContent() { + OperationRequest request = this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, + "content".getBytes(), new HttpHeaders(), Collections.emptyList()); OperationRequest preprocessed = this.preprocessor.preprocess(request); - assertThat(preprocessed.getContent(), is(equalTo("modified".getBytes()))); + assertThat(preprocessed.getContent()).isEqualTo("modified".getBytes()); } @Test - public void modifyResponseContent() { - OperationResponse response = this.responseFactory.create(HttpStatus.OK, - new HttpHeaders(), "content".getBytes()); + void modifyResponseContent() { + OperationResponse response = this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), + "content".getBytes()); OperationResponse preprocessed = this.preprocessor.preprocess(response); - assertThat(preprocessed.getContent(), is(equalTo("modified".getBytes()))); + assertThat(preprocessed.getContent()).isEqualTo("modified".getBytes()); } @Test - public void contentLengthIsUpdated() { + void contentLengthIsUpdated() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentLength(7); - OperationRequest request = this.requestFactory.create( - URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), - httpHeaders, new Parameters(), - Collections.emptyList()); + OperationRequest request = this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, + "content".getBytes(), httpHeaders, Collections.emptyList()); OperationRequest preprocessed = this.preprocessor.preprocess(request); - assertThat(preprocessed.getHeaders().getContentLength(), is(equalTo(8L))); + assertThat(preprocessed.getHeaders().getContentLength()).isEqualTo(8L); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java index 8b3bf232d..a395d6402 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,11 @@ import java.util.Arrays; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.operation.OperationRequest; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -32,10 +31,10 @@ * * @author Andy Wilkinson */ -public class DelegatingOperationRequestPreprocessorTests { +class DelegatingOperationRequestPreprocessorTests { @Test - public void delegationOccurs() { + void delegationOccurs() { OperationRequest originalRequest = mock(OperationRequest.class); OperationPreprocessor preprocessor1 = mock(OperationPreprocessor.class); OperationRequest preprocessedRequest1 = mock(OperationRequest.class); @@ -45,16 +44,14 @@ public void delegationOccurs() { OperationRequest preprocessedRequest3 = mock(OperationRequest.class); given(preprocessor1.preprocess(originalRequest)).willReturn(preprocessedRequest1); - given(preprocessor2.preprocess(preprocessedRequest1)) - .willReturn(preprocessedRequest2); - given(preprocessor3.preprocess(preprocessedRequest2)) - .willReturn(preprocessedRequest3); + given(preprocessor2.preprocess(preprocessedRequest1)).willReturn(preprocessedRequest2); + given(preprocessor3.preprocess(preprocessedRequest2)).willReturn(preprocessedRequest3); OperationRequest result = new DelegatingOperationRequestPreprocessor( Arrays.asList(preprocessor1, preprocessor2, preprocessor3)) - .preprocess(originalRequest); + .preprocess(originalRequest); - assertThat(result, is(preprocessedRequest3)); + assertThat(result).isSameAs(preprocessedRequest3); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java index 03fbc5097..513eeb869 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,11 @@ import java.util.Arrays; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.operation.OperationResponse; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -32,10 +31,10 @@ * * @author Andy Wilkinson */ -public class DelegatingOperationResponsePreprocessorTests { +class DelegatingOperationResponsePreprocessorTests { @Test - public void delegationOccurs() { + void delegationOccurs() { OperationResponse originalResponse = mock(OperationResponse.class); OperationPreprocessor preprocessor1 = mock(OperationPreprocessor.class); OperationResponse preprocessedResponse1 = mock(OperationResponse.class); @@ -44,17 +43,15 @@ public void delegationOccurs() { OperationPreprocessor preprocessor3 = mock(OperationPreprocessor.class); OperationResponse preprocessedResponse3 = mock(OperationResponse.class); - given(preprocessor1.preprocess(originalResponse)) - .willReturn(preprocessedResponse1); - given(preprocessor2.preprocess(preprocessedResponse1)) - .willReturn(preprocessedResponse2); - given(preprocessor3.preprocess(preprocessedResponse2)) - .willReturn(preprocessedResponse3); + given(preprocessor1.preprocess(originalResponse)).willReturn(preprocessedResponse1); + given(preprocessor2.preprocess(preprocessedResponse1)).willReturn(preprocessedResponse2); + given(preprocessor3.preprocess(preprocessedResponse2)).willReturn(preprocessedResponse3); OperationResponse result = new DelegatingOperationResponsePreprocessor( Arrays.asList(preprocessor1, preprocessor2, preprocessor3)) - .preprocess(originalResponse); + .preprocess(originalResponse); - assertThat(result, is(preprocessedResponse3)); + assertThat(result).isSameAs(preprocessedResponse3); } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java deleted file mode 100644 index eb5a6df21..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.operation.preprocess; - -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Test; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.Parameters; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.hasEntry; -import static org.junit.Assert.assertThat; - -/** - * Tests for {@link HeaderRemovingOperationPreprocessorTests}. - * - * @author Andy Wilkinson - * @author Roland Huss - */ -public class HeaderRemovingOperationPreprocessorTests { - - private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - - private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - - private final HeaderRemovingOperationPreprocessor preprocessor = new HeaderRemovingOperationPreprocessor( - new ExactMatchHeaderFilter("b")); - - @Test - public void modifyRequestHeaders() { - OperationRequest request = this.requestFactory.create( - URI.create("http://localhost"), HttpMethod.GET, new byte[0], - getHttpHeaders(), new Parameters(), - Collections.emptyList()); - OperationRequest preprocessed = this.preprocessor.preprocess(request); - assertThat(preprocessed.getHeaders().size(), is(equalTo(2))); - assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); - assertThat(preprocessed.getHeaders(), - hasEntry("Host", Arrays.asList("localhost"))); - } - - @Test - public void modifyResponseHeaders() { - OperationResponse response = createResponse(); - OperationResponse preprocessed = this.preprocessor.preprocess(response); - assertThat(preprocessed.getHeaders().size(), is(equalTo(1))); - assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); - } - - @Test - public void modifyWithPattern() { - OperationResponse response = createResponse("content-length", "1234"); - HeaderRemovingOperationPreprocessor processor = new HeaderRemovingOperationPreprocessor( - new PatternMatchHeaderFilter("co.*le(.)gth]")); - OperationResponse preprocessed = processor.preprocess(response); - assertThat(preprocessed.getHeaders().size(), is(equalTo(2))); - assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); - assertThat(preprocessed.getHeaders(), - hasEntry("b", Arrays.asList("bravo", "banana"))); - } - - @Test - public void removeAllHeaders() { - HeaderRemovingOperationPreprocessor processor = new HeaderRemovingOperationPreprocessor( - new PatternMatchHeaderFilter(".*")); - OperationResponse preprocessed = processor.preprocess(createResponse()); - assertThat(preprocessed.getHeaders().size(), is(equalTo(0))); - } - - private OperationResponse createResponse(String... extraHeaders) { - return this.responseFactory.create(HttpStatus.OK, getHttpHeaders(extraHeaders), - new byte[0]); - } - - private HttpHeaders getHttpHeaders(String... extraHeaders) { - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add("a", "alpha"); - httpHeaders.add("b", "bravo"); - httpHeaders.add("b", "banana"); - for (int i = 0; i < extraHeaders.length; i += 2) { - httpHeaders.add(extraHeaders[i], extraHeaders[i + 1]); - } - return httpHeaders; - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java new file mode 100644 index 000000000..b68b27ff9 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.operation.preprocess; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link HeadersModifyingOperationPreprocessor}. + * + * @author Jihoon Cha + * @author Andy Wilkinson + */ +class HeadersModifyingOperationPreprocessorTests { + + private final HeadersModifyingOperationPreprocessor preprocessor = new HeadersModifyingOperationPreprocessor(); + + @Test + void addNewHeader() { + this.preprocessor.add("a", "alpha"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders().get("a")) + .isEqualTo(Arrays.asList("alpha")); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders().get("a")) + .isEqualTo(Arrays.asList("alpha")); + } + + @Test + void addValueToExistingHeader() { + this.preprocessor.add("a", "alpha"); + assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerSet()).contains(entry("a", Arrays.asList("apple", "alpha"))); + assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerSet()).contains(entry("a", Arrays.asList("apple", "alpha"))); + } + + @Test + void setNewHeader() { + this.preprocessor.set("a", "alpha", "avocado"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders().headerSet()) + .contains(entry("a", Arrays.asList("alpha", "avocado"))); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders().headerSet()) + .contains(entry("a", Arrays.asList("alpha", "avocado"))); + } + + @Test + void setExistingHeader() { + this.preprocessor.set("a", "alpha", "avocado"); + assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerSet()).contains(entry("a", Arrays.asList("alpha", "avocado"))); + assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerSet()).contains(entry("a", Arrays.asList("alpha", "avocado"))); + } + + @Test + void removeNonExistentHeader() { + this.preprocessor.remove("a"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders().headerNames()).doesNotContain("a"); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders().headerNames()).doesNotContain("a"); + } + + @Test + void removeHeader() { + this.preprocessor.remove("a"); + assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerNames()).doesNotContain("a"); + assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerNames()).doesNotContain("a"); + } + + @Test + void removeHeaderValueForNonExistentHeader() { + this.preprocessor.remove("a", "apple"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders().headerNames()).doesNotContain("a"); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders().headerNames()).doesNotContain("a"); + } + + @Test + void removeHeaderValueWithMultipleValues() { + this.preprocessor.remove("a", "apple"); + assertThat( + this.preprocessor.preprocess(createRequest((headers) -> headers.addAll("a", List.of("apple", "alpha")))) + .getHeaders() + .headerSet()) + .contains(entry("a", Arrays.asList("alpha"))); + assertThat(this.preprocessor + .preprocess(createResponse((headers) -> headers.addAll("a", List.of("apple", "alpha")))) + .getHeaders() + .headerSet()).contains(entry("a", Arrays.asList("alpha"))); + } + + @Test + void removeHeaderValueWithSingleValueRemovesEntryEntirely() { + this.preprocessor.remove("a", "apple"); + assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerNames()).doesNotContain("a"); + assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))) + .getHeaders() + .headerNames()).doesNotContain("a"); + } + + @Test + void removeHeadersByNamePattern() { + Consumer headersCustomizer = (headers) -> { + headers.add("apple", "apple"); + headers.add("alpha", "alpha"); + headers.add("avocado", "avocado"); + headers.add("bravo", "bravo"); + }; + this.preprocessor.removeMatching("^a.*"); + assertThat(this.preprocessor.preprocess(createRequest(headersCustomizer)).getHeaders().headerNames()) + .containsOnly("Host", "bravo"); + assertThat(this.preprocessor.preprocess(createResponse(headersCustomizer)).getHeaders().headerNames()) + .containsOnly("bravo"); + } + + private OperationRequest createRequest() { + return createRequest(null); + } + + private OperationRequest createRequest(Consumer headersCustomizer) { + HttpHeaders headers = new HttpHeaders(); + if (headersCustomizer != null) { + headersCustomizer.accept(headers); + } + return new OperationRequestFactory().create(URI.create("http://localhost:8080"), HttpMethod.GET, new byte[0], + headers, Collections.emptyList()); + } + + private OperationResponse createResponse() { + return createResponse(null); + } + + private OperationResponse createResponse(Consumer headersCustomizer) { + HttpHeaders headers = new HttpHeaders(); + if (headersCustomizer != null) { + headersCustomizer.accept(headers); + } + return new OperationResponseFactory().create(HttpStatus.OK, headers, new byte[0]); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java index 6b676d298..7fe262d13 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,13 +26,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.hypermedia.Link; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link LinkMaskingContentModifier}. @@ -40,62 +38,60 @@ * @author Andy Wilkinson * */ -public class LinkMaskingContentModifierTests { +class LinkMaskingContentModifierTests { private final ContentModifier contentModifier = new LinkMaskingContentModifier(); - private final Link[] links = new Link[] { new Link("a", "alpha"), - new Link("b", "bravo") }; + private final Link[] links = new Link[] { new Link("a", "alpha"), new Link("b", "bravo") }; - private final Link[] maskedLinks = new Link[] { new Link("a", "..."), - new Link("b", "...") }; + private final Link[] maskedLinks = new Link[] { new Link("a", "..."), new Link("b", "...") }; @Test - public void halLinksAreMasked() throws Exception { - assertThat( - this.contentModifier.modifyContent(halPayloadWithLinks(this.links), null), - is(equalTo(halPayloadWithLinks(this.maskedLinks)))); + void halLinksAreMasked() throws Exception { + assertThat(this.contentModifier.modifyContent(halPayloadWithLinks(this.links), null)) + .isEqualTo(halPayloadWithLinks(this.maskedLinks)); } @Test - public void formattedHalLinksAreMasked() throws Exception { - assertThat( - this.contentModifier - .modifyContent(formattedHalPayloadWithLinks(this.links), null), - is(equalTo(formattedHalPayloadWithLinks(this.maskedLinks)))); + void formattedHalLinksAreMasked() throws Exception { + assertThat(this.contentModifier.modifyContent(formattedHalPayloadWithLinks(this.links), null)) + .isEqualTo(formattedHalPayloadWithLinks(this.maskedLinks)); + } + + @Test + void atomLinksAreMasked() throws Exception { + assertThat(this.contentModifier.modifyContent(atomPayloadWithLinks(this.links), null)) + .isEqualTo(atomPayloadWithLinks(this.maskedLinks)); } @Test - public void atomLinksAreMasked() throws Exception { - assertThat(this.contentModifier.modifyContent(atomPayloadWithLinks(this.links), - null), is(equalTo(atomPayloadWithLinks(this.maskedLinks)))); + void formattedAtomLinksAreMasked() throws Exception { + assertThat(this.contentModifier.modifyContent(formattedAtomPayloadWithLinks(this.links), null)) + .isEqualTo(formattedAtomPayloadWithLinks(this.maskedLinks)); } @Test - public void formattedAtomLinksAreMasked() throws Exception { + void maskCanBeCustomized() throws Exception { assertThat( - this.contentModifier - .modifyContent(formattedAtomPayloadWithLinks(this.links), null), - is(equalTo(formattedAtomPayloadWithLinks(this.maskedLinks)))); + new LinkMaskingContentModifier("custom").modifyContent(formattedAtomPayloadWithLinks(this.links), null)) + .isEqualTo(formattedAtomPayloadWithLinks(new Link("a", "custom"), new Link("b", "custom"))); } @Test - public void maskCanBeCustomized() throws Exception { + void maskCanUseUtf8Characters() throws Exception { + String ellipsis = "\u2026"; assertThat( - new LinkMaskingContentModifier("custom") - .modifyContent(formattedAtomPayloadWithLinks(this.links), null), - is(equalTo(formattedAtomPayloadWithLinks(new Link("a", "custom"), - new Link("b", "custom"))))); + new LinkMaskingContentModifier(ellipsis).modifyContent(formattedHalPayloadWithLinks(this.links), null)) + .isEqualTo(formattedHalPayloadWithLinks(new Link("a", ellipsis), new Link("b", ellipsis))); } private byte[] atomPayloadWithLinks(Link... links) throws JsonProcessingException { return new ObjectMapper().writeValueAsBytes(createAtomPayload(links)); } - private byte[] formattedAtomPayloadWithLinks(Link... links) - throws JsonProcessingException { + private byte[] formattedAtomPayloadWithLinks(Link... links) throws JsonProcessingException { return new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true) - .writeValueAsBytes(createAtomPayload(links)); + .writeValueAsBytes(createAtomPayload(links)); } private AtomPayload createAtomPayload(Link... links) { @@ -108,10 +104,9 @@ private byte[] halPayloadWithLinks(Link... links) throws JsonProcessingException return new ObjectMapper().writeValueAsBytes(createHalPayload(links)); } - private byte[] formattedHalPayloadWithLinks(Link... links) - throws JsonProcessingException { + private byte[] formattedHalPayloadWithLinks(Link... links) throws JsonProcessingException { return new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true) - .writeValueAsBytes(createHalPayload(links)); + .writeValueAsBytes(createHalPayload(links)); } private HalPayload createHalPayload(Link... links) { @@ -126,11 +121,10 @@ private HalPayload createHalPayload(Link... links) { return payload; } - private static final class AtomPayload { + public static final class AtomPayload { private List links; - @SuppressWarnings("unused") public List getLinks() { return this.links; } @@ -141,7 +135,7 @@ public void setLinks(List links) { } - private static final class HalPayload { + public static final class HalPayload { private Map links; @@ -155,4 +149,5 @@ public void setLinks(Map links) { } } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java deleted file mode 100644 index 432672e76..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.operation.preprocess; - -import java.net.URI; -import java.util.Collections; - -import org.junit.Test; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.hasEntry; -import static org.junit.Assert.assertThat; - -/** - * Tests for {@link ParametersModifyingOperationPreprocessor}. - * - * @author Andy Wilkinson - */ -public class ParametersModifyingOperationPreprocessorTests { - - private final ParametersModifyingOperationPreprocessor preprocessor = new ParametersModifyingOperationPreprocessor(); - - @Test - public void addNewParameter() { - Parameters parameters = new Parameters(); - assertThat(this.preprocessor.add("a", "alpha") - .preprocess(createRequest(parameters)).getParameters(), - hasEntry(equalTo("a"), contains("alpha"))); - } - - @Test - public void addValueToExistingParameter() { - Parameters parameters = new Parameters(); - parameters.add("a", "apple"); - assertThat( - this.preprocessor.add("a", "alpha").preprocess(createRequest(parameters)) - .getParameters(), - hasEntry(equalTo("a"), contains("apple", "alpha"))); - } - - @Test - public void setNewParameter() { - Parameters parameters = new Parameters(); - assertThat( - this.preprocessor.set("a", "alpha", "avocado") - .preprocess(createRequest(parameters)).getParameters(), - hasEntry(equalTo("a"), contains("alpha", "avocado"))); - } - - @Test - public void setExistingParameter() { - Parameters parameters = new Parameters(); - parameters.add("a", "apple"); - assertThat( - this.preprocessor.set("a", "alpha", "avocado") - .preprocess(createRequest(parameters)).getParameters(), - hasEntry(equalTo("a"), contains("alpha", "avocado"))); - } - - @Test - public void removeNonExistentParameter() { - Parameters parameters = new Parameters(); - assertThat(this.preprocessor.remove("a").preprocess(createRequest(parameters)) - .getParameters().size(), is(equalTo(0))); - } - - @Test - public void removeParameter() { - Parameters parameters = new Parameters(); - parameters.add("a", "apple"); - assertThat( - this.preprocessor.set("a", "alpha", "avocado") - .preprocess(createRequest(parameters)).getParameters(), - hasEntry(equalTo("a"), contains("alpha", "avocado"))); - } - - @Test - public void removeParameterValueForNonExistentParameter() { - Parameters parameters = new Parameters(); - assertThat( - this.preprocessor.remove("a", "apple") - .preprocess(createRequest(parameters)).getParameters().size(), - is(equalTo(0))); - } - - @Test - public void removeParameterValueWithMultipleValues() { - Parameters parameters = new Parameters(); - parameters.add("a", "apple"); - parameters.add("a", "alpha"); - assertThat( - this.preprocessor.remove("a", "apple") - .preprocess(createRequest(parameters)).getParameters(), - hasEntry(equalTo("a"), contains("alpha"))); - } - - @Test - public void removeParameterValueWithSingleValueRemovesEntryEntirely() { - Parameters parameters = new Parameters(); - parameters.add("a", "apple"); - assertThat( - this.preprocessor.remove("a", "apple") - .preprocess(createRequest(parameters)).getParameters().size(), - is(equalTo(0))); - } - - private OperationRequest createRequest(Parameters parameters) { - return new OperationRequestFactory().create(URI.create("http://localhost:8080"), - HttpMethod.GET, new byte[0], new HttpHeaders(), parameters, - Collections.emptyList()); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java index 49cdfa2a2..61c24b77b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,59 +17,59 @@ package org.springframework.restdocs.operation.preprocess; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link PatternReplacingContentModifier}. * * @author Andy Wilkinson - * */ -public class PatternReplacingContentModifierTests { +class PatternReplacingContentModifierTests { @Test - public void patternsAreReplaced() throws Exception { - Pattern pattern = Pattern.compile( - "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + void patternsAreReplaced() { + Pattern pattern = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", Pattern.CASE_INSENSITIVE); - PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier( - pattern, "<>"); - assertThat(contentModifier.modifyContent( - "{\"id\" : \"CA761232-ED42-11CE-BACD-00AA0057B223\"}".getBytes(), null), - is(equalTo("{\"id\" : \"<>\"}".getBytes()))); + PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier(pattern, "<>"); + assertThat( + contentModifier.modifyContent("{\"id\" : \"CA761232-ED42-11CE-BACD-00AA0057B223\"}".getBytes(), null)) + .isEqualTo("{\"id\" : \"<>\"}".getBytes()); } @Test - public void contentThatDoesNotMatchIsUnchanged() throws Exception { - Pattern pattern = Pattern.compile( - "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + void contentThatDoesNotMatchIsUnchanged() { + Pattern pattern = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", Pattern.CASE_INSENSITIVE); - PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier( - pattern, "<>"); - assertThat( - contentModifier.modifyContent( - "{\"id\" : \"CA76-ED42-11CE-BACD\"}".getBytes(), null), - is(equalTo("{\"id\" : \"CA76-ED42-11CE-BACD\"}".getBytes()))); + PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier(pattern, "<>"); + assertThat(contentModifier.modifyContent("{\"id\" : \"CA76-ED42-11CE-BACD\"}".getBytes(), null)) + .isEqualTo("{\"id\" : \"CA76-ED42-11CE-BACD\"}".getBytes()); } @Test - public void encodingIsPreserved() { + void encodingIsPreservedUsingCharsetFromContentType() { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; Pattern pattern = Pattern.compile("[0-9]+"); - PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier( - pattern, "<>"); - assertThat( - contentModifier.modifyContent((japaneseContent + " 123").getBytes(), - new MediaType("text", "plain", Charset.forName("UTF-8"))), - is(equalTo((japaneseContent + " <>").getBytes()))); + PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier(pattern, "<>"); + assertThat(contentModifier.modifyContent((japaneseContent + " 123").getBytes(), + new MediaType("text", "plain", Charset.forName("UTF-8")))) + .isEqualTo((japaneseContent + " <>").getBytes()); + } + + @Test + void encodingIsPreservedUsingFallbackCharset() { + String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; + Pattern pattern = Pattern.compile("[0-9]+"); + PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier(pattern, "<>", + StandardCharsets.UTF_8); + assertThat(contentModifier.modifyContent((japaneseContent + " 123").getBytes(), new MediaType("text", "plain"))) + .isEqualTo((japaneseContent + " <>").getBytes()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index c47ee02de..a0c28376a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,15 +20,13 @@ import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.restdocs.test.OutputCapture; +import org.springframework.restdocs.testfixtures.jupiter.CapturedOutput; +import org.springframework.restdocs.testfixtures.jupiter.OutputCaptureExtension; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.isEmptyString; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link PrettyPrintingContentModifier}. @@ -36,53 +34,56 @@ * @author Andy Wilkinson * */ -public class PrettyPrintingContentModifierTests { - - @Rule - public OutputCapture outputCapture = new OutputCapture(); +@ExtendWith(OutputCaptureExtension.class) +class PrettyPrintingContentModifierTests { @Test - public void prettyPrintJson() throws Exception { - assertThat(new PrettyPrintingContentModifier() - .modifyContent("{\"a\":5}".getBytes(), null), - equalTo(String.format("{%n \"a\" : 5%n}").getBytes())); + void prettyPrintJson() { + assertThat(new PrettyPrintingContentModifier().modifyContent("{\"a\":5}".getBytes(), null)) + .isEqualTo(String.format("{%n \"a\" : 5%n}").getBytes()); } @Test - public void prettyPrintXml() throws Exception { - assertThat( - new PrettyPrintingContentModifier().modifyContent( - "".getBytes(), null), - equalTo(String - .format("%n" - + "%n %n%n") - .getBytes())); + void prettyPrintXml() { + assertThat(new PrettyPrintingContentModifier() + .modifyContent("".getBytes(), null)) + .isEqualTo(String + .format("%n" + + "%n %n%n") + .getBytes()); } @Test - public void empytContentIsHandledGracefully() throws Exception { - assertThat(new PrettyPrintingContentModifier().modifyContent("".getBytes(), null), - equalTo("".getBytes())); + void empytContentIsHandledGracefully() { + assertThat(new PrettyPrintingContentModifier().modifyContent("".getBytes(), null)).isEqualTo("".getBytes()); } @Test - public void nonJsonAndNonXmlContentIsHandledGracefully() throws Exception { + void nonJsonAndNonXmlContentIsHandledGracefully(CapturedOutput output) { String content = "abcdefg"; - this.outputCapture.expect(isEmptyString()); - assertThat(new PrettyPrintingContentModifier().modifyContent(content.getBytes(), - null), equalTo(content.getBytes())); + assertThat(new PrettyPrintingContentModifier().modifyContent(content.getBytes(), null)) + .isEqualTo(content.getBytes()); + assertThat(output).isEmpty(); + } + @Test + void nonJsonContentThatInitiallyLooksLikeJsonIsHandledGracefully(CapturedOutput output) { + String content = "\"abc\",\"def\""; + assertThat(new PrettyPrintingContentModifier().modifyContent(content.getBytes(), null)) + .isEqualTo(content.getBytes()); + assertThat(output).isEmpty(); } @Test - public void encodingIsPreserved() throws Exception { + void encodingIsPreserved() throws Exception { Map input = new HashMap<>(); input.put("japanese", "\u30b3\u30f3\u30c6\u30f3\u30c4"); ObjectMapper objectMapper = new ObjectMapper(); @SuppressWarnings("unchecked") - Map output = objectMapper - .readValue(new PrettyPrintingContentModifier().modifyContent( - objectMapper.writeValueAsBytes(input), null), Map.class); - assertThat(output, is(equalTo(input))); + Map output = objectMapper.readValue( + new PrettyPrintingContentModifier().modifyContent(objectMapper.writeValueAsBytes(input), null), + Map.class); + assertThat(output).isEqualTo(input); } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessorTests.java new file mode 100644 index 000000000..0c5724eb7 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessorTests.java @@ -0,0 +1,352 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.operation.preprocess; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.RequestCookie; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link UriModifyingOperationPreprocessor}. + * + * @author Andy Wilkinson + */ +class UriModifyingOperationPreprocessorTests { + + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + + private final UriModifyingOperationPreprocessor preprocessor = new UriModifyingOperationPreprocessor(); + + @Test + void requestUriSchemeCanBeModified() { + this.preprocessor.scheme("https"); + OperationRequest processed = this.preprocessor.preprocess(createRequestWithUri("http://localhost:12345")); + assertThat(processed.getUri()).isEqualTo(URI.create("https://localhost:12345")); + } + + @Test + void requestUriHostCanBeModified() { + this.preprocessor.host("api.example.com"); + OperationRequest processed = this.preprocessor.preprocess(createRequestWithUri("https://api.foo.com:12345")); + assertThat(processed.getUri()).isEqualTo(URI.create("https://api.example.com:12345")); + assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST)).isEqualTo("api.example.com:12345"); + } + + @Test + void requestUriPortCanBeModified() { + this.preprocessor.port(23456); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("https://api.example.com:12345")); + assertThat(processed.getUri()).isEqualTo(URI.create("https://api.example.com:23456")); + assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST)).isEqualTo("api.example.com:23456"); + } + + @Test + void requestUriPortCanBeRemoved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("https://api.example.com:12345")); + assertThat(processed.getUri()).isEqualTo(URI.create("https://api.example.com")); + assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST)).isEqualTo("api.example.com"); + } + + @Test + void requestUriPathIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("https://api.example.com:12345/foo/bar")); + assertThat(processed.getUri()).isEqualTo(URI.create("https://api.example.com/foo/bar")); + } + + @Test + void requestUriQueryIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("https://api.example.com:12345?foo=bar")); + assertThat(processed.getUri()).isEqualTo(URI.create("https://api.example.com?foo=bar")); + } + + @Test + void requestUriAnchorIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("https://api.example.com:12345#foo")); + assertThat(processed.getUri()).isEqualTo(URI.create("https://api.example.com#foo")); + } + + @Test + void requestContentUriSchemeCanBeModified() { + this.preprocessor.scheme("https"); + OperationRequest processed = this.preprocessor.preprocess(createRequestWithContent( + "The uri 'https://localhost:12345' should be used. foo:bar will be unaffected")); + assertThat(new String(processed.getContent())) + .isEqualTo("The uri 'https://localhost:12345' should be used. foo:bar will be unaffected"); + } + + @Test + void requestContentUriHostCanBeModified() { + this.preprocessor.host("api.example.com"); + OperationRequest processed = this.preprocessor.preprocess(createRequestWithContent( + "The uri 'https://localhost:12345' should be used. foo:bar will be unaffected")); + assertThat(new String(processed.getContent())) + .isEqualTo("The uri 'https://api.example.com:12345' should be used. foo:bar will be unaffected"); + } + + @Test + void requestContentHostOfUriWithoutPortCanBeModified() { + this.preprocessor.host("api.example.com"); + OperationRequest processed = this.preprocessor.preprocess( + createRequestWithContent("The uri 'https://localhost' should be used. foo:bar will be unaffected")); + assertThat(new String(processed.getContent())) + .isEqualTo("The uri 'https://api.example.com' should be used. foo:bar will be unaffected"); + } + + @Test + void requestContentUriPortCanBeAdded() { + this.preprocessor.port(23456); + OperationRequest processed = this.preprocessor.preprocess( + createRequestWithContent("The uri 'http://localhost' should be used. foo:bar will be unaffected")); + assertThat(new String(processed.getContent())) + .isEqualTo("The uri 'http://localhost:23456' should be used. foo:bar will be unaffected"); + } + + @Test + void requestContentUriPortCanBeModified() { + this.preprocessor.port(23456); + OperationRequest processed = this.preprocessor.preprocess(createRequestWithContent( + "The uri 'http://localhost:12345' should be used. foo:bar will be unaffected")); + assertThat(new String(processed.getContent())) + .isEqualTo("The uri 'http://localhost:23456' should be used. foo:bar will be unaffected"); + } + + @Test + void requestContentUriPortCanBeRemoved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor.preprocess(createRequestWithContent( + "The uri 'http://localhost:12345' should be used. foo:bar will be unaffected")); + assertThat(new String(processed.getContent())) + .isEqualTo("The uri 'http://localhost' should be used. foo:bar will be unaffected"); + } + + @Test + void multipleRequestContentUrisCanBeModified() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor.preprocess(createRequestWithContent( + "Use 'http://localhost:12345' or 'https://localhost:23456' to access the service")); + assertThat(new String(processed.getContent())) + .isEqualTo("Use 'http://localhost' or 'https://localhost' to access the service"); + } + + @Test + void requestContentUriPathIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345/foo/bar' should be used")); + assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost/foo/bar' should be used"); + } + + @Test + void requestContentUriQueryIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345?foo=bar' should be used")); + assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost?foo=bar' should be used"); + } + + @Test + void requestContentUriAnchorIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345#foo' should be used")); + assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost#foo' should be used"); + } + + @Test + void responseContentUriSchemeCanBeModified() { + this.preprocessor.scheme("https"); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent())).isEqualTo("The uri 'https://localhost:12345' should be used"); + } + + @Test + void responseContentUriHostCanBeModified() { + this.preprocessor.host("api.example.com"); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'https://localhost:12345' should be used")); + assertThat(new String(processed.getContent())) + .isEqualTo("The uri 'https://api.example.com:12345' should be used"); + } + + @Test + void responseContentUriPortCanBeModified() { + this.preprocessor.port(23456); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost:23456' should be used"); + } + + @Test + void responseContentUriPortCanBeRemoved() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost' should be used"); + } + + @Test + void multipleResponseContentUrisCanBeModified() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor.preprocess(createResponseWithContent( + "Use 'http://localhost:12345' or 'https://localhost:23456' to access the service")); + assertThat(new String(processed.getContent())) + .isEqualTo("Use 'http://localhost' or 'https://localhost' to access the service"); + } + + @Test + void responseContentUriPathIsPreserved() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345/foo/bar' should be used")); + assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost/foo/bar' should be used"); + } + + @Test + void responseContentUriQueryIsPreserved() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345?foo=bar' should be used")); + assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost?foo=bar' should be used"); + } + + @Test + void responseContentUriAnchorIsPreserved() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345#foo' should be used")); + assertThat(new String(processed.getContent())).isEqualTo("The uri 'http://localhost#foo' should be used"); + } + + @Test + void urisInRequestHeadersCanBeModified() { + OperationRequest processed = this.preprocessor.host("api.example.com") + .preprocess(createRequestWithHeader("Foo", "https://locahost:12345")); + assertThat(processed.getHeaders().getFirst("Foo")).isEqualTo("https://api.example.com:12345"); + assertThat(processed.getHeaders().getFirst("Host")).isEqualTo("api.example.com"); + } + + @Test + void urisInResponseHeadersCanBeModified() { + OperationResponse processed = this.preprocessor.host("api.example.com") + .preprocess(createResponseWithHeader("Foo", "https://locahost:12345")); + assertThat(processed.getHeaders().getFirst("Foo")).isEqualTo("https://api.example.com:12345"); + } + + @Test + void urisInRequestPartHeadersCanBeModified() { + OperationRequest processed = this.preprocessor.host("api.example.com") + .preprocess(createRequestWithPartWithHeader("Foo", "https://locahost:12345")); + assertThat(processed.getParts().iterator().next().getHeaders().getFirst("Foo")) + .isEqualTo("https://api.example.com:12345"); + } + + @Test + void urisInRequestPartContentCanBeModified() { + OperationRequest processed = this.preprocessor.host("api.example.com") + .preprocess(createRequestWithPartWithContent("The uri 'https://localhost:12345' should be used")); + assertThat(new String(processed.getParts().iterator().next().getContent())) + .isEqualTo("The uri 'https://api.example.com:12345' should be used"); + } + + @Test + void modifiedUriDoesNotGetDoubleEncoded() { + this.preprocessor.scheme("https"); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://localhost:12345?foo=%7B%7D")); + assertThat(processed.getUri()).isEqualTo(URI.create("https://localhost:12345?foo=%7B%7D")); + + } + + @Test + void resultingRequestHasCookiesFromOriginalRequst() { + List cookies = Arrays.asList(new RequestCookie("a", "alpha")); + OperationRequest request = this.requestFactory.create(URI.create("http://localhost:12345"), HttpMethod.GET, + new byte[0], new HttpHeaders(), Collections.emptyList(), cookies); + OperationRequest processed = this.preprocessor.preprocess(request); + assertThat(processed.getCookies().size()).isEqualTo(1); + } + + private OperationRequest createRequestWithUri(String uri) { + return this.requestFactory.create(URI.create(uri), HttpMethod.GET, new byte[0], new HttpHeaders(), + Collections.emptyList()); + } + + private OperationRequest createRequestWithContent(String content) { + return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, content.getBytes(), + new HttpHeaders(), Collections.emptyList()); + } + + private OperationRequest createRequestWithHeader(String name, String value) { + HttpHeaders headers = new HttpHeaders(); + headers.add(name, value); + return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, new byte[0], headers, + Collections.emptyList()); + } + + private OperationRequest createRequestWithPartWithHeader(String name, String value) { + HttpHeaders headers = new HttpHeaders(); + headers.add(name, value); + return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, new byte[0], + new HttpHeaders(), + Arrays.asList(new OperationRequestPartFactory().create("part", "fileName", new byte[0], headers))); + } + + private OperationRequest createRequestWithPartWithContent(String content) { + return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, new byte[0], + new HttpHeaders(), Arrays.asList(new OperationRequestPartFactory().create("part", "fileName", + content.getBytes(), new HttpHeaders()))); + } + + private OperationResponse createResponseWithContent(String content) { + return this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), content.getBytes()); + } + + private OperationResponse createResponseWithHeader(String name, String value) { + HttpHeaders headers = new HttpHeaders(); + headers.add(name, value); + return this.responseFactory.create(HttpStatus.OK, headers, new byte[0]); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java index 3c80be589..ce0945462 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,62 +19,31 @@ import java.io.IOException; import java.util.Arrays; -import org.junit.Rule; -import org.junit.Test; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest.Format; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; -import org.springframework.core.io.FileSystemResource; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; - -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** * Tests for {@link RequestFieldsSnippet} that are specific to Asciidoctor. * * @author Andy Wilkinson */ -public class AsciidoctorRequestFieldsSnippetTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); - - @Test - public void requestFieldsWithListDescription() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")) - .willReturn(snippetResource("request-fields-with-list-description")); - this.snippet.expectRequestFields().withContents( - tableWithHeader(asciidoctor(), "Path", "Type", "Description") - // - .row("a", "String", String.format(" - one%n - two")) - .configuration("[cols=\"1,1,1a\"]")); - - new RequestFieldsSnippet( - Arrays.asList( - fieldWithPath("a").description(Arrays.asList("one", "two")))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .content("{\"a\": \"foo\"}").build()); - } - - private FileSystemResource snippetResource(String name) { - return new FileSystemResource( - "src/test/resources/custom-snippet-templates/asciidoctor/" + name - + ".snippet"); +class AsciidoctorRequestFieldsSnippetTests { + + @RenderedSnippetTest(format = Format.ASCIIDOCTOR) + @SnippetTemplate(snippet = "request-fields", template = "request-fields-with-list-description") + void requestFieldsWithListDescription(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description(Arrays.asList("one", "two")))) + .document(operationBuilder.request("http://localhost").content("{\"a\": \"foo\"}").build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("a", "String", String.format(" - one%n - two")) + .configuration("[cols=\"1,1,1a\"]")); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java new file mode 100644 index 000000000..a5d0b958c --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link FieldPathPayloadSubsectionExtractor}. + * + * @author Andy Wilkinson + */ +class FieldPathPayloadSubsectionExtractorTests { + + @Test + @SuppressWarnings("unchecked") + void extractMapSubsectionOfJsonMap() throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.b") + .extractSubsection("{\"a\":{\"b\":{\"c\":5}}}".getBytes(), MediaType.APPLICATION_JSON); + Map extracted = new ObjectMapper().readValue(extractedPayload, Map.class); + assertThat(extracted.size()).isEqualTo(1); + assertThat(extracted.get("c")).isEqualTo(5); + } + + @Test + @SuppressWarnings("unchecked") + void extractSingleElementArraySubsectionOfJsonMap() throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[]") + .extractSubsection("{\"a\":[{\"b\":5}]}".getBytes(), MediaType.APPLICATION_JSON); + Map extracted = new ObjectMapper().readValue(extractedPayload, Map.class); + assertThat(extracted.size()).isEqualTo(1); + assertThat(extracted).containsOnlyKeys("b"); + } + + @Test + @SuppressWarnings("unchecked") + void extractMultiElementArraySubsectionOfJsonMap() throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a") + .extractSubsection("{\"a\":[{\"b\":5},{\"b\":4}]}".getBytes(), MediaType.APPLICATION_JSON); + Map extracted = new ObjectMapper().readValue(extractedPayload, Map.class); + assertThat(extracted.size()).isEqualTo(1); + assertThat(extracted).containsOnlyKeys("b"); + } + + @Test + @SuppressWarnings("unchecked") + void extractMapSubsectionFromSingleElementArrayInAJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b") + .extractSubsection("{\"a\":[{\"b\":{\"c\":5}}]}".getBytes(), MediaType.APPLICATION_JSON); + Map extracted = new ObjectMapper().readValue(extractedPayload, Map.class); + assertThat(extracted.size()).isEqualTo(1); + assertThat(extracted.get("c")).isEqualTo(5); + } + + @Test + @SuppressWarnings("unchecked") + void extractMapSubsectionWithCommonStructureFromMultiElementArrayInAJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b") + .extractSubsection("{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6}}]}".getBytes(), MediaType.APPLICATION_JSON); + Map extracted = new ObjectMapper().readValue(extractedPayload, Map.class); + assertThat(extracted.size()).isEqualTo(1); + assertThat(extracted).containsOnlyKeys("c"); + } + + @Test + void extractMapSubsectionWithVaryingStructureFromMultiElementArrayInAJsonMap() { + assertThatExceptionOfType(PayloadHandlingException.class) + .isThrownBy(() -> new FieldPathPayloadSubsectionExtractor("a.[].b").extractSubsection( + "{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6, \"d\": 7}}]}".getBytes(), MediaType.APPLICATION_JSON)) + .withMessageContaining("The following non-optional uncommon paths were found: [a.[].b.d]"); + } + + @Test + void extractMapSubsectionWithVaryingStructureFromInconsistentJsonMap() { + assertThatExceptionOfType(PayloadHandlingException.class) + .isThrownBy(() -> new FieldPathPayloadSubsectionExtractor("*.d").extractSubsection( + "{\"a\":{\"b\":1},\"c\":{\"d\":{\"e\":1,\"f\":2}}}".getBytes(), MediaType.APPLICATION_JSON)) + .withMessageContaining("The following non-optional uncommon paths were found: [*.d, *.d.e, *.d.f]"); + } + + @Test + void extractMapSubsectionWithVaryingStructureFromInconsistentJsonMapWhereAllSubsectionFieldsAreOptional() { + assertThatExceptionOfType(PayloadHandlingException.class) + .isThrownBy(() -> new FieldPathPayloadSubsectionExtractor("*.d").extractSubsection( + "{\"a\":{\"b\":1},\"c\":{\"d\":{\"e\":1,\"f\":2}}}".getBytes(), MediaType.APPLICATION_JSON, + Arrays.asList(new FieldDescriptor("e").optional(), new FieldDescriptor("f").optional()))) + .withMessageContaining("The following non-optional uncommon paths were found: [*.d]"); + } + + @Test + @SuppressWarnings("unchecked") + void extractMapSubsectionWithVaryingStructureDueToOptionalFieldsFromMultiElementArrayInAJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b").extractSubsection( + "{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6, \"d\": 7}}]}".getBytes(), MediaType.APPLICATION_JSON, + Arrays.asList(new FieldDescriptor("d").optional())); + Map extracted = new ObjectMapper().readValue(extractedPayload, Map.class); + assertThat(extracted.size()).isEqualTo(1); + assertThat(extracted).containsOnlyKeys("c"); + } + + @Test + @SuppressWarnings("unchecked") + void extractMapSubsectionWithVaryingStructureDueToOptionalParentFieldsFromMultiElementArrayInAJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b").extractSubsection( + "{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6, \"d\": { \"e\": 7}}}]}".getBytes(), + MediaType.APPLICATION_JSON, Arrays.asList(new FieldDescriptor("d").optional())); + Map extracted = new ObjectMapper().readValue(extractedPayload, Map.class); + assertThat(extracted.size()).isEqualTo(1); + assertThat(extracted).containsOnlyKeys("c"); + } + + @Test + void extractedSubsectionIsPrettyPrintedWhenInputIsPrettyPrinted() + throws JsonParseException, JsonMappingException, JsonProcessingException, IOException { + ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); + byte[] prettyPrintedPayload = objectMapper + .writeValueAsBytes(objectMapper.readValue("{\"a\": { \"b\": { \"c\": 1 }}}", Object.class)); + byte[] extractedSubsection = new FieldPathPayloadSubsectionExtractor("a.b") + .extractSubsection(prettyPrintedPayload, MediaType.APPLICATION_JSON); + byte[] prettyPrintedSubsection = objectMapper + .writeValueAsBytes(objectMapper.readValue("{\"c\": 1 }", Object.class)); + assertThat(new String(extractedSubsection)).isEqualTo(new String(prettyPrintedSubsection)); + } + + @Test + void extractedSubsectionIsNotPrettyPrintedWhenInputIsNotPrettyPrinted() + throws JsonParseException, JsonMappingException, JsonProcessingException, IOException { + ObjectMapper objectMapper = new ObjectMapper(); + byte[] payload = objectMapper + .writeValueAsBytes(objectMapper.readValue("{\"a\": { \"b\": { \"c\": 1 }}}", Object.class)); + byte[] extractedSubsection = new FieldPathPayloadSubsectionExtractor("a.b").extractSubsection(payload, + MediaType.APPLICATION_JSON); + byte[] subsection = objectMapper.writeValueAsBytes(objectMapper.readValue("{\"c\": 1 }", Object.class)); + assertThat(new String(extractedSubsection)).isEqualTo(new String(subsection)); + } + + @Test + void extractNonExistentSubsection() { + assertThatThrownBy(() -> new FieldPathPayloadSubsectionExtractor("a.c") + .extractSubsection("{\"a\":{\"b\":{\"c\":5}}}".getBytes(), MediaType.APPLICATION_JSON)) + .isInstanceOf(PayloadHandlingException.class) + .hasMessage("a.c does not identify a section of the payload"); + } + + @Test + void extractEmptyArraySubsection() { + assertThatThrownBy(() -> new FieldPathPayloadSubsectionExtractor("a") + .extractSubsection("{\"a\":[]}}".getBytes(), MediaType.APPLICATION_JSON)) + .isInstanceOf(PayloadHandlingException.class) + .hasMessage("a identifies an empty section of the payload"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java new file mode 100644 index 000000000..da5b1e902 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link FieldTypeResolver}. + * + * @author Mathias Düsterhöft + */ +public class FieldTypeResolverTests { + + @Test + public void whenForContentWithDescriptorsCalledWithJsonContentThenReturnsJsonFieldTypeResolver() { + assertThat(FieldTypeResolver.forContentWithDescriptors("{\"field\": \"value\"}".getBytes(), + MediaType.APPLICATION_JSON, Collections.emptyList())) + .isInstanceOf(JsonContentHandler.class); + } + + @Test + public void whenForContentWithDescriptorsCalledWithXmlContentThenReturnsXmlContentHandler() { + assertThat(FieldTypeResolver.forContentWithDescriptors("5".getBytes(), MediaType.APPLICATION_XML, + Collections.emptyList())) + .isInstanceOf(XmlContentHandler.class); + } + + @Test + public void whenForContentWithDescriptorsIsCalledWithInvalidContentThenExceptionIsThrown() { + assertThatExceptionOfType(PayloadHandlingException.class).isThrownBy(() -> FieldTypeResolver + .forContentWithDescriptors("some".getBytes(), MediaType.APPLICATION_XML, Collections.emptyList())); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java index 1d3fc9fe7..43f0100af 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,39 +16,193 @@ package org.springframework.restdocs.payload; -import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link JsonContentHandler}. * * @author Andy Wilkinson + * @author Mathias Düsterhöft */ -public class JsonContentHandlerTests { +class JsonContentHandlerTests { - @Rule - public ExpectedException thrown = ExpectedException.none(); + @Test + void typeForFieldWithNullValueMustMatch() { + FieldDescriptor descriptor = new FieldDescriptor("a").type(JsonFieldType.STRING); + assertThatExceptionOfType(FieldTypesDoNotMatchException.class) + .isThrownBy(() -> new JsonContentHandler("{\"a\": null}".getBytes(), Arrays.asList(descriptor)) + .resolveFieldType(descriptor)); + } + + @Test + void typeForFieldWithNotNullAndThenNullValueMustMatch() { + FieldDescriptor descriptor = new FieldDescriptor("a[].id").type(JsonFieldType.STRING); + assertThatExceptionOfType(FieldTypesDoNotMatchException.class).isThrownBy( + () -> new JsonContentHandler("{\"a\":[{\"id\":1},{\"id\":null}]}".getBytes(), Arrays.asList(descriptor)) + .resolveFieldType(descriptor)); + } + + @Test + void typeForFieldWithNullAndThenNotNullValueMustMatch() { + FieldDescriptor descriptor = new FieldDescriptor("a.[].id").type(JsonFieldType.STRING); + assertThatExceptionOfType(FieldTypesDoNotMatchException.class).isThrownBy( + () -> new JsonContentHandler("{\"a\":[{\"id\":null},{\"id\":1}]}".getBytes(), Arrays.asList(descriptor)) + .resolveFieldType(descriptor)); + } + + @Test + void typeForOptionalFieldWithNumberAndThenNullValueIsNumber() { + FieldDescriptor descriptor = new FieldDescriptor("a[].id").optional(); + Object fieldType = new JsonContentHandler("{\"a\":[{\"id\":1},{\"id\":null}]}\"".getBytes(), + Arrays.asList(descriptor)) + .resolveFieldType(descriptor); + assertThat((JsonFieldType) fieldType).isEqualTo(JsonFieldType.NUMBER); + } + + @Test + void typeForOptionalFieldWithNullAndThenNumberIsNumber() { + FieldDescriptor descriptor = new FieldDescriptor("a[].id").optional(); + Object fieldType = new JsonContentHandler("{\"a\":[{\"id\":null},{\"id\":1}]}".getBytes(), + Arrays.asList(descriptor)) + .resolveFieldType(descriptor); + assertThat((JsonFieldType) fieldType).isEqualTo(JsonFieldType.NUMBER); + } + + @Test + void typeForFieldWithNumberAndThenNullValueIsVaries() { + FieldDescriptor descriptor = new FieldDescriptor("a[].id"); + Object fieldType = new JsonContentHandler("{\"a\":[{\"id\":1},{\"id\":null}]}\"".getBytes(), + Arrays.asList(descriptor)) + .resolveFieldType(descriptor); + assertThat((JsonFieldType) fieldType).isEqualTo(JsonFieldType.VARIES); + } + + @Test + void typeForFieldWithNullAndThenNumberIsVaries() { + FieldDescriptor descriptor = new FieldDescriptor("a[].id"); + Object fieldType = new JsonContentHandler("{\"a\":[{\"id\":null},{\"id\":1}]}".getBytes(), + Arrays.asList(descriptor)) + .resolveFieldType(descriptor); + assertThat((JsonFieldType) fieldType).isEqualTo(JsonFieldType.VARIES); + } + + @Test + void typeForOptionalFieldWithNullValueCanBeProvidedExplicitly() { + FieldDescriptor descriptor = new FieldDescriptor("a").type(JsonFieldType.STRING).optional(); + Object fieldType = new JsonContentHandler("{\"a\": null}".getBytes(), Arrays.asList(descriptor)) + .resolveFieldType(descriptor); + assertThat((JsonFieldType) fieldType).isEqualTo(JsonFieldType.STRING); + } + + @Test + void typeForFieldWithSometimesPresentOptionalAncestorCanBeProvidedExplicitly() { + FieldDescriptor descriptor = new FieldDescriptor("a.[].b.c").type(JsonFieldType.NUMBER); + FieldDescriptor ancestor = new FieldDescriptor("a.[].b").optional(); + Object fieldType = new JsonContentHandler("{\"a\":[ { \"d\": 4}, {\"b\":{\"c\":5}, \"d\": 4}]}".getBytes(), + Arrays.asList(descriptor, ancestor)) + .resolveFieldType(descriptor); + assertThat((JsonFieldType) fieldType).isEqualTo(JsonFieldType.NUMBER); + } + + @Test + void failsFastWithNonJsonContent() { + assertThatExceptionOfType(PayloadHandlingException.class) + .isThrownBy(() -> new JsonContentHandler("Non-JSON content".getBytes(), Collections.emptyList())); + } + + @Test + void describedFieldThatIsNotPresentIsConsideredMissing() { + List descriptors = Arrays.asList(new FieldDescriptor("a"), new FieldDescriptor("b"), + new FieldDescriptor("c")); + List missingFields = new JsonContentHandler("{\"a\": \"alpha\", \"b\":\"bravo\"}".getBytes(), + descriptors) + .findMissingFields(); + assertThat(missingFields.size()).isEqualTo(1); + assertThat(missingFields.get(0).getPath()).isEqualTo("c"); + } + + @Test + void describedOptionalFieldThatIsNotPresentIsNotConsideredMissing() { + List descriptors = Arrays.asList(new FieldDescriptor("a"), new FieldDescriptor("b"), + new FieldDescriptor("c").optional()); + List missingFields = new JsonContentHandler("{\"a\": \"alpha\", \"b\":\"bravo\"}".getBytes(), + descriptors) + .findMissingFields(); + assertThat(missingFields.size()).isEqualTo(0); + } + + @Test + void describedFieldThatIsNotPresentNestedBeneathOptionalFieldThatIsPresentIsConsideredMissing() { + List descriptors = Arrays.asList(new FieldDescriptor("a").optional(), new FieldDescriptor("b"), + new FieldDescriptor("a.c")); + List missingFields = new JsonContentHandler("{\"a\":\"alpha\",\"b\":\"bravo\"}".getBytes(), + descriptors) + .findMissingFields(); + assertThat(missingFields.size()).isEqualTo(1); + assertThat(missingFields.get(0).getPath()).isEqualTo("a.c"); + } + + @Test + void describedFieldThatIsNotPresentNestedBeneathOptionalFieldThatIsNotPresentIsNotConsideredMissing() { + List descriptors = Arrays.asList(new FieldDescriptor("a").optional(), new FieldDescriptor("b"), + new FieldDescriptor("a.c")); + List missingFields = new JsonContentHandler("{\"b\":\"bravo\"}".getBytes(), descriptors) + .findMissingFields(); + assertThat(missingFields.size()).isEqualTo(0); + } + + @Test + void describedFieldThatIsNotPresentNestedBeneathOptionalArrayThatIsEmptyIsNotConsideredMissing() { + List descriptors = Arrays.asList(new FieldDescriptor("outer"), + new FieldDescriptor("outer[]").optional(), new FieldDescriptor("outer[].inner")); + List missingFields = new JsonContentHandler("{\"outer\":[]}".getBytes(), descriptors) + .findMissingFields(); + assertThat(missingFields.size()).isEqualTo(0); + } + + @Test + void describedSometimesPresentFieldThatIsChildOfSometimesPresentOptionalArrayIsNotConsideredMissing() { + List descriptors = Arrays.asList(new FieldDescriptor("a.[].c").optional(), + new FieldDescriptor("a.[].c.d")); + List missingFields = new JsonContentHandler( + "{\"a\":[ {\"b\": \"bravo\"}, {\"b\": \"bravo\", \"c\": { \"d\": \"delta\"}}]}".getBytes(), descriptors) + .findMissingFields(); + assertThat(missingFields.size()).isEqualTo(0); + } + + @Test + void describedMissingFieldThatIsChildOfNestedOptionalArrayThatIsEmptyIsNotConsideredMissing() { + List descriptors = Arrays.asList(new FieldDescriptor("a.[].b").optional(), + new FieldDescriptor("a.[].b.[]").optional(), new FieldDescriptor("a.[].b.[].c")); + List missingFields = new JsonContentHandler("{\"a\":[{\"b\":[]}]}".getBytes(), descriptors) + .findMissingFields(); + assertThat(missingFields.size()).isEqualTo(0); + } @Test - public void typeForFieldWithNullValueMustMatch() throws IOException { - this.thrown.expect(FieldTypesDoNotMatchException.class); - new JsonContentHandler("{\"a\": null}".getBytes()) - .determineFieldType(new FieldDescriptor("a").type(JsonFieldType.STRING)); + void describedMissingFieldThatIsChildOfNestedOptionalArrayThatContainsAnObjectIsConsideredMissing() { + List descriptors = Arrays.asList(new FieldDescriptor("a.[].b").optional(), + new FieldDescriptor("a.[].b.[]").optional(), new FieldDescriptor("a.[].b.[].c")); + List missingFields = new JsonContentHandler("{\"a\":[{\"b\":[{}]}]}".getBytes(), descriptors) + .findMissingFields(); + assertThat(missingFields.size()).isEqualTo(1); + assertThat(missingFields.get(0).getPath()).isEqualTo("a.[].b.[].c"); } @Test - public void typeForOptionalFieldWithNullValueDoesNotHaveToMatch() throws IOException { - Object fieldType = new JsonContentHandler("{\"a\": null}".getBytes()) - .determineFieldType( - new FieldDescriptor("a").type(JsonFieldType.STRING).optional()); - assertThat((JsonFieldType) fieldType, is(equalTo(JsonFieldType.STRING))); + void describedMissingFieldThatIsChildOfOptionalObjectThatIsNullIsNotConsideredMissing() { + List descriptors = Arrays.asList(new FieldDescriptor("a").optional(), + new FieldDescriptor("a.b")); + List missingFields = new JsonContentHandler("{\"a\":null}".getBytes(), descriptors) + .findMissingFields(); + assertThat(missingFields.size()).isEqualTo(0); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java index a7bc9d81e..69324c659 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,11 @@ package org.springframework.restdocs.payload; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.hamcrest.Matchers.contains; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; +import org.springframework.restdocs.payload.JsonFieldPath.PathType; + +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link JsonFieldPath}. @@ -29,120 +28,132 @@ * @author Andy Wilkinson * @author Jeremy Rickard */ -public class JsonFieldPathTests { +class JsonFieldPathTests { @Test - public void singleFieldIsPreciseAndNotAnArray() { + void pathTypeOfSingleFieldIsSingle() { JsonFieldPath path = JsonFieldPath.compile("a"); - assertTrue(path.isPrecise()); - assertFalse(path.isArray()); + assertThat(path.getType()).isEqualTo(PathType.SINGLE); } @Test - public void singleNestedFieldIsPreciseAndNotAnArray() { + void pathTypeOfSingleNestedFieldIsSingle() { JsonFieldPath path = JsonFieldPath.compile("a.b"); - assertTrue(path.isPrecise()); - assertFalse(path.isArray()); + assertThat(path.getType()).isEqualTo(PathType.SINGLE); } @Test - public void topLevelArrayIsPreciseAndAnArray() { + void pathTypeOfTopLevelArrayIsSingle() { JsonFieldPath path = JsonFieldPath.compile("[]"); - assertTrue(path.isPrecise()); - assertTrue(path.isArray()); + assertThat(path.getType()).isEqualTo(PathType.SINGLE); } @Test - public void fieldBeneathTopLevelArrayIsNotPreciseAndNotAnArray() { + void pathTypeOfFieldBeneathTopLevelArrayIsMulti() { JsonFieldPath path = JsonFieldPath.compile("[]a"); - assertFalse(path.isPrecise()); - assertFalse(path.isArray()); + assertThat(path.getType()).isEqualTo(PathType.MULTI); } @Test - public void arrayIsPreciseAndAnArray() { + void pathTypeOfSingleNestedArrayIsSingle() { JsonFieldPath path = JsonFieldPath.compile("a[]"); - assertTrue(path.isPrecise()); - assertTrue(path.isArray()); + assertThat(path.getType()).isEqualTo(PathType.SINGLE); } @Test - public void nestedArrayIsPreciseAndAnArray() { + void pathTypeOfArrayBeneathNestedFieldsIsSingle() { JsonFieldPath path = JsonFieldPath.compile("a.b[]"); - assertTrue(path.isPrecise()); - assertTrue(path.isArray()); + assertThat(path.getType()).isEqualTo(PathType.SINGLE); } @Test - public void arrayOfArraysIsNotPreciseAndIsAnArray() { + void pathTypeOfArrayOfArraysIsMulti() { JsonFieldPath path = JsonFieldPath.compile("a[][]"); - assertFalse(path.isPrecise()); - assertTrue(path.isArray()); + assertThat(path.getType()).isEqualTo(PathType.MULTI); } @Test - public void fieldBeneathAnArrayIsNotPreciseAndIsNotAnArray() { + void pathTypeOfFieldBeneathAnArrayIsMulti() { JsonFieldPath path = JsonFieldPath.compile("a[].b"); - assertFalse(path.isPrecise()); - assertFalse(path.isArray()); + assertThat(path.getType()).isEqualTo(PathType.MULTI); + } + + @Test + void pathTypeOfFieldBeneathTopLevelWildcardIsMulti() { + JsonFieldPath path = JsonFieldPath.compile("*.a"); + assertThat(path.getType()).isEqualTo(PathType.MULTI); + } + + @Test + void pathTypeOfFieldBeneathNestedWildcardIsMulti() { + JsonFieldPath path = JsonFieldPath.compile("a.*.b"); + assertThat(path.getType()).isEqualTo(PathType.MULTI); + } + + @Test + void pathTypeOfLeafWidlcardIsMulti() { + JsonFieldPath path = JsonFieldPath.compile("a.*"); + assertThat(path.getType()).isEqualTo(PathType.MULTI); + } + + @Test + void compilationOfSingleElementPath() { + assertThat(JsonFieldPath.compile("a").getSegments()).containsExactly("a"); + } + + @Test + void compilationOfMultipleElementPath() { + assertThat(JsonFieldPath.compile("a.b.c").getSegments()).containsExactly("a", "b", "c"); } @Test - public void compilationOfSingleElementPath() { - assertThat(JsonFieldPath.compile("a").getSegments(), contains("a")); + void compilationOfPathWithArraysWithNoDotSeparators() { + assertThat(JsonFieldPath.compile("a[]b[]c").getSegments()).containsExactly("a", "[]", "b", "[]", "c"); } @Test - public void compilationOfMultipleElementPath() { - assertThat(JsonFieldPath.compile("a.b.c").getSegments(), contains("a", "b", "c")); + void compilationOfPathWithArraysWithPreAndPostDotSeparators() { + assertThat(JsonFieldPath.compile("a.[].b.[].c").getSegments()).containsExactly("a", "[]", "b", "[]", "c"); } @Test - public void compilationOfPathWithArraysWithNoDotSeparators() { - assertThat(JsonFieldPath.compile("a[]b[]c").getSegments(), - contains("a", "[]", "b", "[]", "c")); + void compilationOfPathWithArraysWithPreDotSeparators() { + assertThat(JsonFieldPath.compile("a.[]b.[]c").getSegments()).containsExactly("a", "[]", "b", "[]", "c"); } @Test - public void compilationOfPathWithArraysWithPreAndPostDotSeparators() { - assertThat(JsonFieldPath.compile("a.[].b.[].c").getSegments(), - contains("a", "[]", "b", "[]", "c")); + void compilationOfPathWithArraysWithPostDotSeparators() { + assertThat(JsonFieldPath.compile("a[].b[].c").getSegments()).containsExactly("a", "[]", "b", "[]", "c"); } @Test - public void compilationOfPathWithArraysWithPreDotSeparators() { - assertThat(JsonFieldPath.compile("a.[]b.[]c").getSegments(), - contains("a", "[]", "b", "[]", "c")); + void compilationOfPathStartingWithAnArray() { + assertThat(JsonFieldPath.compile("[]a.b.c").getSegments()).containsExactly("[]", "a", "b", "c"); } @Test - public void compilationOfPathWithArraysWithPostDotSeparators() { - assertThat(JsonFieldPath.compile("a[].b[].c").getSegments(), - contains("a", "[]", "b", "[]", "c")); + void compilationOfMultipleElementPathWithBrackets() { + assertThat(JsonFieldPath.compile("['a']['b']['c']").getSegments()).containsExactly("a", "b", "c"); } @Test - public void compilationOfPathStartingWithAnArray() { - assertThat(JsonFieldPath.compile("[]a.b.c").getSegments(), - contains("[]", "a", "b", "c")); + void compilationOfMultipleElementPathWithAndWithoutBrackets() { + assertThat(JsonFieldPath.compile("['a'][].b['c']").getSegments()).containsExactly("a", "[]", "b", "c"); } @Test - public void compilationOfMultipleElementPathWithBrackets() { - assertThat(JsonFieldPath.compile("['a']['b']['c']").getSegments(), - contains("a", "b", "c")); + void compilationOfMultipleElementPathWithAndWithoutBracketsAndEmbeddedDots() { + assertThat(JsonFieldPath.compile("['a.key'][].b['c']").getSegments()).containsExactly("a.key", "[]", "b", "c"); } @Test - public void compilationOfMultipleElementPathWithAndWithoutBrackets() { - assertThat(JsonFieldPath.compile("['a'][].b['c']").getSegments(), - contains("a", "[]", "b", "c")); + void compilationOfPathWithAWildcard() { + assertThat(JsonFieldPath.compile("a.b.*.c").getSegments()).containsExactly("a", "b", "*", "c"); } @Test - public void compilationOfMultipleElementPathWithAndWithoutBracketsAndEmbeddedDots() { - assertThat(JsonFieldPath.compile("['a.key'][].b['c']").getSegments(), - contains("a.key", "[]", "b", "c")); + void compilationOfPathWithAWildcardInBrackets() { + assertThat(JsonFieldPath.compile("a.b.['*'].c").getSegments()).containsExactly("a", "b", "*", "c"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathsTests.java new file mode 100644 index 000000000..2e770649c --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathsTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.Arrays; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JsonFieldPaths}. + * + * @author Andy Wilkinson + */ +class JsonFieldPathsTests { + + @Test + void noUncommonPathsForSingleItem() { + assertThat( + JsonFieldPaths.from(Arrays.asList(json("{\"a\": 1, \"b\": [ { \"c\": 2}, {\"c\": 3}, {\"c\": null}]}"))) + .getUncommon()) + .isEmpty(); + } + + @Test + void noUncommonPathsForMultipleIdenticalItems() { + Object item = json("{\"a\": 1, \"b\": [ { \"c\": 2}, {\"c\": 3} ]}"); + assertThat(JsonFieldPaths.from(Arrays.asList(item, item)).getUncommon()).isEmpty(); + } + + @Test + void noUncommonPathsForMultipleMatchingItemsWithDifferentScalarValues() { + assertThat(JsonFieldPaths.from(Arrays.asList(json("{\"a\": 1, \"b\": [ { \"c\": 2}, {\"c\": 3} ]}"), + json("{\"a\": 4, \"b\": [ { \"c\": 5}, {\"c\": 6} ]}"))) + .getUncommon()).isEmpty(); + } + + @Test + void missingEntryInMapIsIdentifiedAsUncommon() { + assertThat( + JsonFieldPaths.from(Arrays.asList(json("{\"a\": 1}"), json("{\"a\": 1}"), json("{\"a\": 1, \"b\": 2}"))) + .getUncommon()) + .containsExactly("b"); + } + + @Test + void missingEntryInNestedMapIsIdentifiedAsUncommon() { + assertThat(JsonFieldPaths.from(Arrays.asList(json("{\"a\": 1, \"b\": {\"c\": 1}}"), + json("{\"a\": 1, \"b\": {\"c\": 1}}"), json("{\"a\": 1, \"b\": {\"c\": 1, \"d\": 2}}"))) + .getUncommon()).containsExactly("b.d"); + } + + @Test + void missingEntriesInNestedMapAreIdentifiedAsUncommon() { + assertThat(JsonFieldPaths.from(Arrays.asList(json("{\"a\": 1, \"b\": {\"c\": 1}}"), + json("{\"a\": 1, \"b\": {\"c\": 1}}"), json("{\"a\": 1, \"b\": {\"d\": 2}}"))) + .getUncommon()).containsExactly("b.c", "b.d"); + } + + @Test + void absentItemFromFieldExtractionCausesAllPresentFieldsToBeIdentifiedAsUncommon() { + assertThat(JsonFieldPaths + .from(Arrays.asList(ExtractedField.ABSENT, json("{\"a\": 1, \"b\": {\"c\": 1}}"), + json("{\"a\": 1, \"b\": {\"c\": 1}}"), json("{\"a\": 1, \"b\": {\"d\": 2}}"))) + .getUncommon()).containsExactly("", "a", "b", "b.c", "b.d"); + } + + @Test + void missingEntryBeneathArrayIsIdentifiedAsUncommon() { + assertThat(JsonFieldPaths + .from(Arrays.asList(json("[{\"b\": 1}]"), json("[{\"b\": 1}]"), json("[{\"b\": 1, \"c\": 2}]"))) + .getUncommon()).containsExactly("[].c"); + } + + @Test + void missingEntryBeneathNestedArrayIsIdentifiedAsUncommon() { + assertThat(JsonFieldPaths.from(Arrays.asList(json("{\"a\": [{\"b\": 1}]}"), json("{\"a\": [{\"b\": 1}]}"), + json("{\"a\": [{\"b\": 1, \"c\": 2}]}"))) + .getUncommon()).containsExactly("a.[].c"); + } + + private Object json(String json) { + try { + return new ObjectMapper().readValue(json, Object.class); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index 976fc30cf..b87099857 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,225 +19,459 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertThat; +import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField; + +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link JsonFieldProcessor}. * * @author Andy Wilkinson */ -public class JsonFieldProcessorTests { +class JsonFieldProcessorTests { private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); @Test - public void extractTopLevelMapEntry() { + void extractTopLevelMapEntry() { Map payload = new HashMap<>(); payload.put("a", "alpha"); - assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a"), payload), - equalTo((Object) "alpha")); + assertThat(this.fieldProcessor.extract("a", payload).getValue()).isEqualTo("alpha"); } @Test - public void extractNestedMapEntry() { + void extractNestedMapEntry() { Map payload = new HashMap<>(); Map alpha = new HashMap<>(); payload.put("a", alpha); alpha.put("b", "bravo"); - assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload), - equalTo((Object) "bravo")); + assertThat(this.fieldProcessor.extract("a.b", payload).getValue()).isEqualTo("bravo"); } @Test - public void extractTopLevelArray() { + void extractTopLevelArray() { List> payload = new ArrayList<>(); Map bravo = new HashMap<>(); bravo.put("b", "bravo"); payload.add(bravo); payload.add(bravo); - assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("[]"), payload), - equalTo((Object) payload)); + assertThat(this.fieldProcessor.extract("[]", payload).getValue()).isEqualTo(payload); } @Test - public void extractArray() { + void extractArray() { Map payload = new HashMap<>(); Map bravo = new HashMap<>(); bravo.put("b", "bravo"); List> alpha = Arrays.asList(bravo, bravo); payload.put("a", alpha); - assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a"), payload), - equalTo((Object) alpha)); + assertThat(this.fieldProcessor.extract("a", payload).getValue()).isEqualTo(alpha); } @Test - public void extractArrayContents() { + void extractArrayContents() { Map payload = new HashMap<>(); Map bravo = new HashMap<>(); bravo.put("b", "bravo"); List> alpha = Arrays.asList(bravo, bravo); payload.put("a", alpha); - assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload), - equalTo((Object) alpha)); + assertThat(this.fieldProcessor.extract("a[]", payload).getValue()).isEqualTo(alpha); } @Test - public void extractFromItemsInArray() { + void extractFromItemsInArray() { Map payload = new HashMap<>(); Map entry = new HashMap<>(); entry.put("b", "bravo"); List> alpha = Arrays.asList(entry, entry); payload.put("a", alpha); - assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[].b"), payload), - equalTo((Object) Arrays.asList("bravo", "bravo"))); + assertThat(this.fieldProcessor.extract("a[].b", payload).getValue()).isEqualTo(Arrays.asList("bravo", "bravo")); } @Test - public void extractNestedArray() { + void extractOccasionallyAbsentFieldFromItemsInArray() { + Map payload = new HashMap<>(); + Map entry = new HashMap<>(); + entry.put("b", "bravo"); + List> alpha = Arrays.asList(entry, new HashMap()); + payload.put("a", alpha); + assertThat(this.fieldProcessor.extract("a[].b", payload).getValue()) + .isEqualTo(Arrays.asList("bravo", ExtractedField.ABSENT)); + } + + @Test + void extractOccasionallyNullFieldFromItemsInArray() { + Map payload = new HashMap<>(); + Map nonNullField = new HashMap<>(); + nonNullField.put("b", "bravo"); + Map nullField = new HashMap<>(); + nullField.put("b", null); + List> alpha = Arrays.asList(nonNullField, nullField); + payload.put("a", alpha); + assertThat(this.fieldProcessor.extract("a[].b", payload).getValue()).isEqualTo(Arrays.asList("bravo", null)); + } + + @Test + void extractNestedArray() { Map payload = new HashMap<>(); Map entry1 = createEntry("id:1"); Map entry2 = createEntry("id:2"); Map entry3 = createEntry("id:3"); - List>> alpha = Arrays - .asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3)); + List>> alpha = Arrays.asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3)); payload.put("a", alpha); - assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][]"), payload), - equalTo((Object) Arrays.asList(entry1, entry2, entry3))); + assertThat(this.fieldProcessor.extract("a[][]", payload).getValue()) + .isEqualTo(Arrays.asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3))); } @Test - public void extractFromItemsInNestedArray() { + void extractFromItemsInNestedArray() { Map payload = new HashMap<>(); Map entry1 = createEntry("id:1"); Map entry2 = createEntry("id:2"); Map entry3 = createEntry("id:3"); - List>> alpha = Arrays - .asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3)); + List>> alpha = Arrays.asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3)); payload.put("a", alpha); - assertThat( - this.fieldProcessor.extract(JsonFieldPath.compile("a[][].id"), payload), - equalTo((Object) Arrays.asList("1", "2", "3"))); + assertThat(this.fieldProcessor.extract("a[][].id", payload).getValue()).isEqualTo(Arrays.asList("1", "2", "3")); } @Test - public void extractArraysFromItemsInNestedArray() { + void extractArraysFromItemsInNestedArray() { Map payload = new HashMap<>(); Map entry1 = createEntry("ids", Arrays.asList(1, 2)); Map entry2 = createEntry("ids", Arrays.asList(3)); Map entry3 = createEntry("ids", Arrays.asList(4)); - List>> alpha = Arrays - .asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3)); + List>> alpha = Arrays.asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3)); payload.put("a", alpha); - assertThat( - this.fieldProcessor.extract(JsonFieldPath.compile("a[][].ids"), payload), - equalTo((Object) Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3), - Arrays.asList(4)))); + assertThat(this.fieldProcessor.extract("a[][].ids", payload).getValue()) + .isEqualTo(Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3), Arrays.asList(4))); } - @Test(expected = FieldDoesNotExistException.class) - public void nonExistentTopLevelField() { - this.fieldProcessor.extract(JsonFieldPath.compile("a"), - new HashMap()); + @Test + void nonExistentTopLevelField() { + assertThat(this.fieldProcessor.extract("a", Collections.emptyMap()).getValue()) + .isEqualTo(ExtractedField.ABSENT); } - @Test(expected = FieldDoesNotExistException.class) - public void nonExistentNestedField() { + @Test + void nonExistentNestedField() { HashMap payload = new HashMap<>(); - payload.put("a", new HashMap()); - this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload); + payload.put("a", new HashMap<>()); + assertThat(this.fieldProcessor.extract("a.b", payload).getValue()).isEqualTo(ExtractedField.ABSENT); } - @Test(expected = FieldDoesNotExistException.class) - public void nonExistentNestedFieldWhenParentIsNotAMap() { + @Test + void nonExistentNestedFieldWhenParentIsNotAMap() { HashMap payload = new HashMap<>(); payload.put("a", 5); - this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload); + assertThat(this.fieldProcessor.extract("a.b", payload).getValue()).isEqualTo(ExtractedField.ABSENT); } - @Test(expected = FieldDoesNotExistException.class) - public void nonExistentFieldWhenParentIsAnArray() { + @Test + void nonExistentFieldWhenParentIsAnArray() { HashMap payload = new HashMap<>(); HashMap alpha = new HashMap<>(); alpha.put("b", Arrays.asList(new HashMap())); payload.put("a", alpha); - this.fieldProcessor.extract(JsonFieldPath.compile("a.b.c"), payload); + assertThat(this.fieldProcessor.extract("a.b.c", payload).getValue()).isEqualTo(ExtractedField.ABSENT); } - @Test(expected = FieldDoesNotExistException.class) - public void nonExistentArrayField() { + @Test + void nonExistentArrayField() { HashMap payload = new HashMap<>(); - this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload); + assertThat(this.fieldProcessor.extract("a[]", payload).getValue()).isEqualTo(ExtractedField.ABSENT); } - @Test(expected = FieldDoesNotExistException.class) - public void nonExistentArrayFieldAsTypeDoesNotMatch() { + @Test + void nonExistentArrayFieldAsTypeDoesNotMatch() { HashMap payload = new HashMap<>(); payload.put("a", 5); - this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload); + assertThat(this.fieldProcessor.extract("a[]", payload).getValue()).isEqualTo(ExtractedField.ABSENT); } - @Test(expected = FieldDoesNotExistException.class) - public void nonExistentFieldBeneathAnArray() { + @Test + void nonExistentFieldBeneathAnArray() { HashMap payload = new HashMap<>(); HashMap alpha = new HashMap<>(); alpha.put("b", Arrays.asList(new HashMap())); payload.put("a", alpha); - this.fieldProcessor.extract(JsonFieldPath.compile("a.b[].id"), payload); + assertThat(this.fieldProcessor.extract("a.b[].id", payload).getValue()) + .isEqualTo(Arrays.asList(ExtractedField.ABSENT)); } @Test - public void removeTopLevelMapEntry() { + void removeTopLevelMapEntry() { Map payload = new HashMap<>(); payload.put("a", "alpha"); - this.fieldProcessor.remove(JsonFieldPath.compile("a"), payload); - assertThat(payload.size(), equalTo(0)); + this.fieldProcessor.remove("a", payload); + assertThat(payload.size()).isEqualTo(0); + } + + @Test + void mapWithEntriesIsNotRemovedWhenNotAlsoRemovingDescendants() { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo"); + this.fieldProcessor.remove("a", payload); + assertThat(payload.size()).isEqualTo(1); } @Test - public void removeNestedMapEntry() { + void removeSubsectionRemovesMapWithEntries() { Map payload = new HashMap<>(); Map alpha = new HashMap<>(); payload.put("a", alpha); alpha.put("b", "bravo"); - this.fieldProcessor.remove(JsonFieldPath.compile("a.b"), payload); - assertThat(payload.size(), equalTo(0)); + this.fieldProcessor.removeSubsection("a", payload); + assertThat(payload.size()).isEqualTo(0); + } + + @Test + void removeNestedMapEntry() { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo"); + this.fieldProcessor.remove("a.b", payload); + assertThat(payload.size()).isEqualTo(0); + } + + @SuppressWarnings("unchecked") + @Test + void removeItemsInArray() throws IOException { + Map payload = new ObjectMapper().readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", + Map.class); + this.fieldProcessor.remove("a[].b", payload); + assertThat(payload.size()).isEqualTo(0); + } + + @SuppressWarnings("unchecked") + @Test + void removeItemsInNestedArray() throws IOException { + Map payload = new ObjectMapper().readValue("{\"a\": [[{\"id\":1},{\"id\":2}], [{\"id\":3}]]}", + Map.class); + this.fieldProcessor.remove("a[][].id", payload); + assertThat(payload.size()).isEqualTo(0); + } + + @SuppressWarnings("unchecked") + @Test + void removeDoesNotRemoveArrayWithMapEntries() throws IOException { + Map payload = new ObjectMapper().readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", + Map.class); + this.fieldProcessor.remove("a[]", payload); + assertThat(payload.size()).isEqualTo(1); + } + + @SuppressWarnings("unchecked") + @Test + void removeDoesNotRemoveArrayWithListEntries() throws IOException { + Map payload = new ObjectMapper().readValue("{\"a\": [[2],[3]]}", Map.class); + this.fieldProcessor.remove("a[]", payload); + assertThat(payload.size()).isEqualTo(1); + } + + @SuppressWarnings("unchecked") + @Test + void removeRemovesArrayWithOnlyScalarEntries() throws IOException { + Map payload = new ObjectMapper().readValue("{\"a\": [\"bravo\", \"charlie\"]}", Map.class); + this.fieldProcessor.remove("a", payload); + assertThat(payload.size()).isEqualTo(0); } @SuppressWarnings("unchecked") @Test - public void removeItemsInArray() throws IOException { - Map payload = new ObjectMapper() - .readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); - this.fieldProcessor.remove(JsonFieldPath.compile("a[].b"), payload); - assertThat(payload.size(), equalTo(0)); + void removeSubsectionRemovesArrayWithMapEntries() throws IOException { + Map payload = new ObjectMapper().readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", + Map.class); + this.fieldProcessor.removeSubsection("a[]", payload); + assertThat(payload.size()).isEqualTo(0); } @SuppressWarnings("unchecked") @Test - public void removeItemsInNestedArray() throws IOException { - Map payload = new ObjectMapper() - .readValue("{\"a\": [[{\"id\":1},{\"id\":2}], [{\"id\":3}]]}", Map.class); - this.fieldProcessor.remove(JsonFieldPath.compile("a[][].id"), payload); - assertThat(payload.size(), equalTo(0)); + void removeSubsectionRemovesArrayWithListEntries() throws IOException { + Map payload = new ObjectMapper().readValue("{\"a\": [[2],[3]]}", Map.class); + this.fieldProcessor.removeSubsection("a[]", payload); + assertThat(payload.size()).isEqualTo(0); } @Test - public void extractNestedEntryWithDotInKeys() throws IOException { + void extractNestedEntryWithDotInKeys() { Map payload = new HashMap<>(); Map alpha = new HashMap<>(); payload.put("a.key", alpha); alpha.put("b.key", "bravo"); - assertThat(this.fieldProcessor - .extract(JsonFieldPath.compile("['a.key']['b.key']"), payload), - equalTo((Object) "bravo")); + assertThat(this.fieldProcessor.extract("['a.key']['b.key']", payload).getValue()).isEqualTo("bravo"); + } + + @SuppressWarnings("unchecked") + @Test + void extractNestedEntriesUsingTopLevelWildcard() { + Map payload = new LinkedHashMap<>(); + Map alpha = new LinkedHashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo1"); + Map charlie = new LinkedHashMap<>(); + charlie.put("b", "bravo2"); + payload.put("c", charlie); + assertThat((List) this.fieldProcessor.extract("*.b", payload).getValue()).containsExactly("bravo1", + "bravo2"); + } + + @SuppressWarnings("unchecked") + @Test + void extractNestedEntriesUsingMidLevelWildcard() { + Map payload = new LinkedHashMap<>(); + Map alpha = new LinkedHashMap<>(); + payload.put("a", alpha); + Map bravo = new LinkedHashMap<>(); + bravo.put("b", "bravo"); + alpha.put("one", bravo); + alpha.put("two", bravo); + assertThat((List) this.fieldProcessor.extract("a.*.b", payload).getValue()).containsExactly("bravo", + "bravo"); + } + + @SuppressWarnings("unchecked") + @Test + void extractUsingLeafWildcardMatchingSingleItem() { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo1"); + Map charlie = new HashMap<>(); + charlie.put("b", "bravo2"); + payload.put("c", charlie); + assertThat((List) this.fieldProcessor.extract("a.*", payload).getValue()).containsExactly("bravo1"); + } + + @SuppressWarnings("unchecked") + @Test + void extractUsingLeafWildcardMatchingMultipleItems() { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo1"); + alpha.put("c", "charlie"); + assertThat((List) this.fieldProcessor.extract("a.*", payload).getValue()).containsExactly("bravo1", + "charlie"); + } + + @Test + void removeUsingLeafWildcard() { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo1"); + alpha.put("c", "charlie"); + this.fieldProcessor.remove("a.*", payload); + assertThat(payload.size()).isEqualTo(0); + } + + @Test + void removeUsingTopLevelWildcard() { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo1"); + alpha.put("c", "charlie"); + this.fieldProcessor.remove("*.b", payload); + assertThat(alpha).doesNotContainKey("b"); + } + + @Test + void removeUsingMidLevelWildcard() { + Map payload = new LinkedHashMap<>(); + Map alpha = new LinkedHashMap<>(); + payload.put("a", alpha); + payload.put("c", "charlie"); + Map bravo1 = new LinkedHashMap<>(); + bravo1.put("b", "bravo"); + alpha.put("one", bravo1); + Map bravo2 = new LinkedHashMap<>(); + bravo2.put("b", "bravo"); + alpha.put("two", bravo2); + this.fieldProcessor.remove("a.*.b", payload); + assertThat(payload.size()).isEqualTo(1); + assertThat(payload).containsEntry("c", "charlie"); + } + + @Test + void hasFieldIsTrueForNonNullFieldInMap() { + Map payload = new HashMap<>(); + payload.put("a", "alpha"); + assertThat(this.fieldProcessor.hasField("a", payload)).isTrue(); + } + + @Test + void hasFieldIsTrueForNullFieldInMap() { + Map payload = new HashMap<>(); + payload.put("a", null); + assertThat(this.fieldProcessor.hasField("a", payload)).isTrue(); + } + + @Test + void hasFieldIsFalseForAbsentFieldInMap() { + Map payload = new HashMap<>(); + payload.put("a", null); + assertThat(this.fieldProcessor.hasField("b", payload)).isFalse(); + } + + @Test + void hasFieldIsTrueForNeverNullFieldBeneathArray() { + Map payload = new HashMap<>(); + Map nested = new HashMap<>(); + nested.put("b", "bravo"); + payload.put("a", Arrays.asList(nested, nested, nested)); + assertThat(this.fieldProcessor.hasField("a.[].b", payload)).isTrue(); + } + + @Test + void hasFieldIsTrueForAlwaysNullFieldBeneathArray() { + Map payload = new HashMap<>(); + Map nested = new HashMap<>(); + nested.put("b", null); + payload.put("a", Arrays.asList(nested, nested, nested)); + assertThat(this.fieldProcessor.hasField("a.[].b", payload)).isTrue(); + } + + @Test + void hasFieldIsFalseForAlwaysAbsentFieldBeneathArray() { + Map payload = new HashMap<>(); + Map nested = new HashMap<>(); + nested.put("b", "bravo"); + payload.put("a", Arrays.asList(nested, nested, nested)); + assertThat(this.fieldProcessor.hasField("a.[].c", payload)).isFalse(); + } + + @Test + void hasFieldIsFalseForOccasionallyAbsentFieldBeneathArray() { + Map payload = new HashMap<>(); + Map nested = new HashMap<>(); + nested.put("b", "bravo"); + payload.put("a", Arrays.asList(nested, new HashMap<>(), nested)); + assertThat(this.fieldProcessor.hasField("a.[].b", payload)).isFalse(); + } + + @Test + void hasFieldIsFalseForOccasionallyNullFieldBeneathArray() { + Map payload = new HashMap<>(); + Map fieldPresent = new HashMap<>(); + fieldPresent.put("b", "bravo"); + Map fieldNull = new HashMap<>(); + fieldNull.put("b", null); + payload.put("a", Arrays.asList(fieldPresent, fieldPresent, fieldNull)); + assertThat(this.fieldProcessor.hasField("a.[].b", payload)).isFalse(); } private Map createEntry(String... pairs) { @@ -254,4 +488,5 @@ private Map createEntry(String key, Object value) { entry.put(key, value); return entry; } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java deleted file mode 100644 index 9e04c867c..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -package org.springframework.restdocs.payload; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertThat; - -/** - * Tests for {@link JsonFieldTypeResolver}. - * - * @author Andy Wilkinson - * - */ -public class JsonFieldTypeResolverTests { - - private final JsonFieldTypeResolver fieldTypeResolver = new JsonFieldTypeResolver(); - - @Rule - public ExpectedException thrownException = ExpectedException.none(); - - @Test - public void arrayField() throws IOException { - assertFieldType(JsonFieldType.ARRAY, "[]"); - } - - @Test - public void topLevelArray() throws IOException { - assertThat( - this.fieldTypeResolver.resolveFieldType("[]", - new ObjectMapper().readValue("[{\"a\":\"alpha\"}]", List.class)), - equalTo(JsonFieldType.ARRAY)); - } - - @Test - public void nestedArray() throws IOException { - assertThat( - this.fieldTypeResolver.resolveFieldType("a[]", - createPayload("{\"a\": [{\"b\":\"bravo\"}]}")), - equalTo(JsonFieldType.ARRAY)); - } - - @Test - public void booleanField() throws IOException { - assertFieldType(JsonFieldType.BOOLEAN, "true"); - } - - @Test - public void objectField() throws IOException { - assertFieldType(JsonFieldType.OBJECT, "{}"); - } - - @Test - public void nullField() throws IOException { - assertFieldType(JsonFieldType.NULL, "null"); - } - - @Test - public void numberField() throws IOException { - assertFieldType(JsonFieldType.NUMBER, "1.2345"); - } - - @Test - public void stringField() throws IOException { - assertFieldType(JsonFieldType.STRING, "\"Foo\""); - } - - @Test - public void nestedField() throws IOException { - assertThat( - this.fieldTypeResolver.resolveFieldType("a.b.c", - createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), - equalTo(JsonFieldType.OBJECT)); - } - - @Test - public void multipleFieldsWithSameType() throws IOException { - assertThat( - this.fieldTypeResolver.resolveFieldType("a[].id", - createPayload("{\"a\":[{\"id\":1},{\"id\":2}]}")), - equalTo(JsonFieldType.NUMBER)); - } - - @Test - public void multipleFieldsWithDifferentTypes() throws IOException { - assertThat( - this.fieldTypeResolver.resolveFieldType("a[].id", - createPayload("{\"a\":[{\"id\":1},{\"id\":true}]}")), - equalTo(JsonFieldType.VARIES)); - } - - @Test - public void nonExistentFieldProducesIllegalArgumentException() throws IOException { - this.thrownException.expect(FieldDoesNotExistException.class); - this.thrownException.expectMessage( - "The payload does not contain a field with the path 'a.b'"); - this.fieldTypeResolver.resolveFieldType("a.b", createPayload("{\"a\":{}}")); - } - - private void assertFieldType(JsonFieldType expectedType, String jsonValue) - throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType("field", - createSimplePayload(jsonValue)), equalTo(expectedType)); - } - - private Map createSimplePayload(String value) throws IOException { - return createPayload("{\"field\":" + value + "}"); - } - - @SuppressWarnings("unchecked") - private Map createPayload(String json) throws IOException { - return new ObjectMapper().readValue(json, Map.class); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesDiscovererTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesDiscovererTests.java new file mode 100644 index 000000000..4c469005b --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesDiscovererTests.java @@ -0,0 +1,186 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link JsonFieldTypesDiscoverer}. + * + * @author Andy Wilkinson + */ +class JsonFieldTypesDiscovererTests { + + private final JsonFieldTypesDiscoverer fieldTypeDiscoverer = new JsonFieldTypesDiscoverer(); + + @Test + void arrayField() throws IOException { + assertThat(discoverFieldTypes("[]")).containsExactly(JsonFieldType.ARRAY); + } + + @Test + void topLevelArray() throws IOException { + assertThat(discoverFieldTypes("[]", "[{\"a\":\"alpha\"}]")).containsExactly(JsonFieldType.ARRAY); + } + + @Test + void nestedArray() throws IOException { + assertThat(discoverFieldTypes("a[]", "{\"a\": [{\"b\":\"bravo\"}]}")).containsExactly(JsonFieldType.ARRAY); + } + + @Test + void arrayNestedBeneathAnArray() throws IOException { + assertThat(discoverFieldTypes("a[].b[]", "{\"a\": [{\"b\": [ 1, 2 ]}]}")).containsExactly(JsonFieldType.ARRAY); + } + + @Test + void specificFieldOfObjectInArrayNestedBeneathAnArray() throws IOException { + assertThat(discoverFieldTypes("a[].b[].c", "{\"a\": [{\"b\": [ {\"c\": 5}, {\"c\": 5}]}]}")) + .containsExactly(JsonFieldType.NUMBER); + } + + @Test + void booleanField() throws IOException { + assertThat(discoverFieldTypes("true")).containsExactly(JsonFieldType.BOOLEAN); + } + + @Test + void objectField() throws IOException { + assertThat(discoverFieldTypes("{}")).containsExactly(JsonFieldType.OBJECT); + } + + @Test + void nullField() throws IOException { + assertThat(discoverFieldTypes("null")).containsExactly(JsonFieldType.NULL); + } + + @Test + void numberField() throws IOException { + assertThat(discoverFieldTypes("1.2345")).containsExactly(JsonFieldType.NUMBER); + } + + @Test + void stringField() throws IOException { + assertThat(discoverFieldTypes("\"Foo\"")).containsExactly(JsonFieldType.STRING); + } + + @Test + void nestedField() throws IOException { + assertThat(discoverFieldTypes("a.b.c", "{\"a\":{\"b\":{\"c\":{}}}}")).containsExactly(JsonFieldType.OBJECT); + } + + @Test + void multipleFieldsWithSameType() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":2}]}")) + .containsExactly(JsonFieldType.NUMBER); + } + + @Test + void multipleFieldsWithDifferentTypes() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":true}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.BOOLEAN); + } + + @Test + void multipleFieldsWithDifferentTypesAndSometimesAbsent() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":true}, {}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.BOOLEAN, JsonFieldType.NULL); + } + + @Test + void multipleFieldsWhenSometimesAbsent() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":2}, {}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.NULL); + } + + @Test + void multipleFieldsWhenSometimesNull() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":2}, {\"id\":null}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.NULL); + } + + @Test + void multipleFieldsWithDifferentTypesAndSometimesNull() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":true}, {\"id\":null}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.BOOLEAN, JsonFieldType.NULL); + } + + @Test + void multipleFieldsWhenEitherNullOrAbsent() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{},{\"id\":null}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NULL); + } + + @Test + void multipleFieldsThatAreAllNull() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":null},{\"id\":null}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NULL); + } + + @Test + void nonExistentSingleFieldProducesFieldDoesNotExistException() { + assertThatExceptionOfType(FieldDoesNotExistException.class) + .isThrownBy(() -> discoverFieldTypes("a.b", "{\"a\":{}}")) + .withMessage("The payload does not contain a field with the path 'a.b'"); + } + + @Test + void nonExistentMultipleFieldsProducesFieldDoesNotExistException() { + assertThatExceptionOfType(FieldDoesNotExistException.class) + .isThrownBy(() -> discoverFieldTypes("a[].b", "{\"a\":[{\"c\":1},{\"c\":2}]}")) + .withMessage("The payload does not contain a field with the path 'a[].b'"); + } + + @Test + void leafWildcardWithCommonType() throws IOException { + assertThat(discoverFieldTypes("a.*", "{\"a\": {\"b\": 5, \"c\": 6}}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER); + } + + @Test + void leafWildcardWithVaryingType() throws IOException { + assertThat(discoverFieldTypes("a.*", "{\"a\": {\"b\": 5, \"c\": \"six\"}}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.STRING); + } + + @Test + void intermediateWildcardWithCommonType() throws IOException { + assertThat(discoverFieldTypes("a.*.d", "{\"a\": {\"b\": {\"d\": 4}, \"c\": {\"d\": 5}}}}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER); + } + + @Test + void intermediateWildcardWithVaryingType() throws IOException { + assertThat(discoverFieldTypes("a.*.d", "{\"a\": {\"b\": {\"d\": 4}, \"c\": {\"d\": \"four\"}}}}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.STRING); + } + + private JsonFieldTypes discoverFieldTypes(String value) throws IOException { + return discoverFieldTypes("field", "{\"field\":" + value + "}"); + } + + private JsonFieldTypes discoverFieldTypes(String path, String json) throws IOException { + return this.fieldTypeDiscoverer.discoverFieldTypes(path, new ObjectMapper().readValue(json, Object.class)); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesTests.java new file mode 100644 index 000000000..1d2a869d4 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.util.EnumSet; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JsonFieldTypes}. + * + * @author Andy Wilkinson + */ +class JsonFieldTypesTests { + + @Test + void singleTypeCoalescesToThatType() { + assertThat(new JsonFieldTypes(JsonFieldType.NUMBER).coalesce(false)).isEqualTo(JsonFieldType.NUMBER); + } + + @Test + void singleTypeCoalescesToThatTypeWhenOptional() { + assertThat(new JsonFieldTypes(JsonFieldType.NUMBER).coalesce(true)).isEqualTo(JsonFieldType.NUMBER); + } + + @Test + void multipleTypesCoalescesToVaries() { + assertThat(new JsonFieldTypes(EnumSet.of(JsonFieldType.ARRAY, JsonFieldType.NUMBER)).coalesce(false)) + .isEqualTo(JsonFieldType.VARIES); + } + + @Test + void nullAndNonNullTypesCoalescesToVaries() { + assertThat(new JsonFieldTypes(EnumSet.of(JsonFieldType.ARRAY, JsonFieldType.NULL)).coalesce(false)) + .isEqualTo(JsonFieldType.VARIES); + } + + @Test + void nullAndNonNullTypesCoalescesToNonNullTypeWhenOptional() { + assertThat(new JsonFieldTypes(EnumSet.of(JsonFieldType.ARRAY, JsonFieldType.NULL)).coalesce(true)) + .isEqualTo(JsonFieldType.ARRAY); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index a6abf8d1c..11b6df5fa 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,9 @@ import java.util.Arrays; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.restdocs.payload.PayloadDocumentation.applyPathPrefix; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.snippet.Attributes.key; @@ -33,61 +31,54 @@ * * @author Andy Wilkinson */ -public class PayloadDocumentationTests { +class PayloadDocumentationTests { @Test - public void applyPathPrefixAppliesPrefixToDescriptorPaths() { + void applyPathPrefixAppliesPrefixToDescriptorPaths() { List descriptors = applyPathPrefix("alpha.", Arrays.asList(fieldWithPath("bravo"), fieldWithPath("charlie"))); - assertThat(descriptors.size(), is(equalTo(2))); - assertThat(descriptors.get(0).getPath(), is(equalTo("alpha.bravo"))); + assertThat(descriptors.size()).isEqualTo(2); + assertThat(descriptors.get(0).getPath()).isEqualTo("alpha.bravo"); } @Test - public void applyPathPrefixCopiesIgnored() { - List descriptors = applyPathPrefix("alpha.", - Arrays.asList(fieldWithPath("bravo").ignored())); - assertThat(descriptors.size(), is(equalTo(1))); - assertThat(descriptors.get(0).isIgnored(), is(true)); + void applyPathPrefixCopiesIgnored() { + List descriptors = applyPathPrefix("alpha.", Arrays.asList(fieldWithPath("bravo").ignored())); + assertThat(descriptors.size()).isEqualTo(1); + assertThat(descriptors.get(0).isIgnored()).isTrue(); } @Test - public void applyPathPrefixCopiesOptional() { - List descriptors = applyPathPrefix("alpha.", - Arrays.asList(fieldWithPath("bravo").optional())); - assertThat(descriptors.size(), is(equalTo(1))); - assertThat(descriptors.get(0).isOptional(), is(true)); + void applyPathPrefixCopiesOptional() { + List descriptors = applyPathPrefix("alpha.", Arrays.asList(fieldWithPath("bravo").optional())); + assertThat(descriptors.size()).isEqualTo(1); + assertThat(descriptors.get(0).isOptional()).isTrue(); } @Test - public void applyPathPrefixCopiesDescription() { + void applyPathPrefixCopiesDescription() { List descriptors = applyPathPrefix("alpha.", Arrays.asList(fieldWithPath("bravo").description("Some field"))); - assertThat(descriptors.size(), is(equalTo(1))); - assertThat(descriptors.get(0).getDescription(), - is(equalTo((Object) "Some field"))); + assertThat(descriptors.size()).isEqualTo(1); + assertThat(descriptors.get(0).getDescription()).isEqualTo("Some field"); } @Test - public void applyPathPrefixCopiesType() { + void applyPathPrefixCopiesType() { List descriptors = applyPathPrefix("alpha.", Arrays.asList(fieldWithPath("bravo").type(JsonFieldType.OBJECT))); - assertThat(descriptors.size(), is(equalTo(1))); - assertThat(descriptors.get(0).getType(), - is(equalTo((Object) JsonFieldType.OBJECT))); + assertThat(descriptors.size()).isEqualTo(1); + assertThat(descriptors.get(0).getType()).isEqualTo(JsonFieldType.OBJECT); } @Test - public void applyPathPrefixCopiesAttributes() { + void applyPathPrefixCopiesAttributes() { List descriptors = applyPathPrefix("alpha.", - Arrays.asList(fieldWithPath("bravo").attributes(key("a").value("alpha"), - key("b").value("bravo")))); - assertThat(descriptors.size(), is(equalTo(1))); - assertThat(descriptors.get(0).getAttributes().size(), is(equalTo(2))); - assertThat(descriptors.get(0).getAttributes().get("a"), - is(equalTo((Object) "alpha"))); - assertThat(descriptors.get(0).getAttributes().get("b"), - is(equalTo((Object) "bravo"))); + Arrays.asList(fieldWithPath("bravo").attributes(key("a").value("alpha"), key("b").value("bravo")))); + assertThat(descriptors.size()).isEqualTo(1); + assertThat(descriptors.get(0).getAttributes().size()).isEqualTo(2); + assertThat(descriptors.get(0).getAttributes().get("a")).isEqualTo("alpha"); + assertThat(descriptors.get(0).getAttributes().get("b")).isEqualTo("bravo"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java new file mode 100644 index 000000000..f9c94dbd9 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link RequestPartBodySnippet}. + * + * @author Andy Wilkinson + */ +class RequestBodyPartSnippetTests { + + @RenderedSnippetTest + void requestPartWithBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestPartBody("one") + .document(operationBuilder.request("http://localhost").part("one", "some content".getBytes()).build()); + assertThat(snippets.requestPartBody("one")) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("some content")); + } + + @RenderedSnippetTest + void requestPartWithNoBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestPartBody("one").document(operationBuilder.request("http://localhost").part("one", new byte[0]).build()); + assertThat(snippets.requestPartBody("one")) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("")); + } + + @RenderedSnippetTest + void requestPartWithJsonMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestPartBody("one").document(operationBuilder.request("http://localhost") + .part("one", "".getBytes()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .build()); + assertThat(snippets.requestPartBody("one")) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("")); + } + + @RenderedSnippetTest + void requestPartWithJsonSubtypeMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestPartBody("one").document(operationBuilder.request("http://localhost") + .part("one", "".getBytes()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PROBLEM_JSON_VALUE) + .build()); + assertThat(snippets.requestPartBody("one")) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("")); + } + + @RenderedSnippetTest + void requestPartWithXmlMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestPartBody("one").document(operationBuilder.request("http://localhost") + .part("one", "".getBytes()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + assertThat(snippets.requestPartBody("one")) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("xml", "nowrap").content("")); + } + + @RenderedSnippetTest + void requestPartWithXmlSubtypeMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestPartBody("one").document(operationBuilder.request("http://localhost") + .part("one", "".getBytes()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_ATOM_XML_VALUE) + .build()); + assertThat(snippets.requestPartBody("one")) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("xml", "nowrap").content("")); + } + + @RenderedSnippetTest + void subsectionOfRequestPartBody(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestPartBody("one", beneathPath("a.b")).document(operationBuilder.request("http://localhost") + .part("one", "{\"a\":{\"b\":{\"c\":5}}}".getBytes()) + .build()); + assertThat(snippets.requestPartBody("one", "beneath-a.b")) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("{\"c\":5}")); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-part-body", template = "request-part-body-with-language") + void customSnippetAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestPartBody("one", attributes(key("language").value("json"))) + .document(operationBuilder.request("http://localhost").part("one", "{\"a\":\"alpha\"}".getBytes()).build()); + assertThat(snippets.requestPartBody("one")).isCodeBlock( + (codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("{\"a\":\"alpha\"}")); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java new file mode 100644 index 000000000..c332fdcc7 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestBody; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link RequestBodySnippet}. + * + * @author Andy Wilkinson + */ +class RequestBodySnippetTests { + + @RenderedSnippetTest + void requestWithBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestBody().document(operationBuilder.request("http://localhost").content("some content").build()); + assertThat(snippets.requestBody()) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("some content")); + } + + @RenderedSnippetTest + void requestWithNoBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestBody().document(operationBuilder.request("http://localhost").build()); + assertThat(snippets.requestBody()).isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("")); + } + + @RenderedSnippetTest + void requestWithJsonMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestBody().document(operationBuilder.request("http://localhost") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .build()); + assertThat(snippets.requestBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("")); + } + + @RenderedSnippetTest + void requestWithJsonSubtypeMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestBody().document(operationBuilder.request("http://localhost") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PROBLEM_JSON_VALUE) + .build()); + assertThat(snippets.requestBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("")); + } + + @RenderedSnippetTest + void requestWithXmlMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestBody().document(operationBuilder.request("http://localhost") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + assertThat(snippets.requestBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("xml", "nowrap").content("")); + } + + @RenderedSnippetTest + void requestWithXmlSubtypeMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestBody().document(operationBuilder.request("http://localhost") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_ATOM_XML_VALUE) + .build()); + assertThat(snippets.requestBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("xml", "nowrap").content("")); + } + + @RenderedSnippetTest + void subsectionOfRequestBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestBody(beneathPath("a.b")) + .document(operationBuilder.request("http://localhost").content("{\"a\":{\"b\":{\"c\":5}}}").build()); + assertThat(snippets.requestBody("beneath-a.b")) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("{\"c\":5}")); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-body", template = "request-body-with-language") + void customSnippetAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestBody(attributes(key("language").value("json"))) + .document(operationBuilder.request("http://localhost").content("{\"a\":\"alpha\"}").build()); + assertThat(snippets.requestBody()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("{\"a\":\"alpha\"}")); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java deleted file mode 100644 index 438d0104f..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.payload; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; - -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; - -/** - * Tests for failures when rendering {@link RequestFieldsSnippet} due to missing or - * undocumented fields. - * - * @author Andy Wilkinson - */ -public class RequestFieldsSnippetFailureTests { - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder( - TemplateFormats.asciidoctor()); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void undocumentedRequestField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(startsWith( - "The following parts of the payload were not" + " documented:")); - new RequestFieldsSnippet(Collections.emptyList()) - .document(this.operationBuilder.request("http://localhost") - .content("{\"a\": 5}").build()); - } - - @Test - public void missingRequestField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a.b]")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(this.operationBuilder.request("http://localhost").content("{}") - .build()); - } - - @Test - public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { - this.thrown.expect(FieldTypeRequiredException.class); - new RequestFieldsSnippet( - Arrays.asList(fieldWithPath("a.b").description("one").optional())) - .document(this.operationBuilder.request("http://localhost") - .content("{ }").build()); - } - - @Test - public void undocumentedRequestFieldAndMissingRequestField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(startsWith( - "The following parts of the payload were not" + " documented:")); - this.thrown - .expectMessage(endsWith("Fields with the following paths were not found" - + " in the payload: [a.b]")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(this.operationBuilder.request("http://localhost") - .content("{ \"a\": { \"c\": 5 }}").build()); - } - - @Test - public void attemptToDocumentFieldsWithNoRequestBody() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage( - equalTo("Cannot document request fields as the request body is empty")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(this.operationBuilder.request("http://localhost").build()); - } - - @Test - public void fieldWithExplicitTypeThatDoesNotMatchThePayload() throws IOException { - this.thrown.expect(FieldTypesDoNotMatchException.class); - this.thrown.expectMessage(equalTo("The documented type of the field 'a' is" - + " Object but the actual type is Number")); - new RequestFieldsSnippet(Arrays - .asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) - .document(this.operationBuilder.request("http://localhost") - .content("{ \"a\": 5 }").build()); - } - - @Test - public void fieldWithExplicitSpecificTypeThatActuallyVaries() throws IOException { - this.thrown.expect(FieldTypesDoNotMatchException.class); - this.thrown.expectMessage(equalTo("The documented type of the field '[].a' is" - + " Object but the actual type is Varies")); - new RequestFieldsSnippet(Arrays.asList( - fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) - .document(this.operationBuilder.request("http://localhost") - .content("[{ \"a\": 5 },{ \"a\": \"b\" }]").build()); - } - - @Test - public void undocumentedXmlRequestField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(startsWith( - "The following parts of the payload were not" + " documented:")); - new RequestFieldsSnippet(Collections.emptyList()) - .document(this.operationBuilder.request("http://localhost") - .content("5").header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void xmlRequestFieldWithNoType() throws IOException { - this.thrown.expect(FieldTypeRequiredException.class); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(this.operationBuilder.request("http://localhost") - .content("5").header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void missingXmlRequestField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/b]")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))).document(this.operationBuilder - .request("http://localhost").content("") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void undocumentedXmlRequestFieldAndMissingXmlRequestField() - throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(startsWith( - "The following parts of the payload were not" + " documented:")); - this.thrown - .expectMessage(endsWith("Fields with the following paths were not found" - + " in the payload: [a/b]")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) - .document(this.operationBuilder.request("http://localhost") - .content("5").header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index e0753c7c2..7c6394d96 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,22 +18,23 @@ import java.io.IOException; import java.util.Arrays; - -import org.junit.Test; +import java.util.Collections; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -41,240 +42,494 @@ * Tests for {@link RequestFieldsSnippet}. * * @author Andy Wilkinson + * @author Sungjun Lee */ -public class RequestFieldsSnippetTests extends AbstractSnippetTests { +class RequestFieldsSnippetTests { - public RequestFieldsSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); + @RenderedSnippetTest + void mapRequestWithFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two"), fieldWithPath("a").description("three"))) + .document(operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one") + .row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); } - @Test - public void mapRequestWithFields() throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") - .row("`a`", "`Object`", "three")); + @RenderedSnippetTest + void mapRequestWithNullField(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(operationBuilder.request("http://localhost").content("{\"a\": {\"b\": null}}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`Null`", "one")); + } - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), - fieldWithPath("a.c").description("two"), - fieldWithPath("a").description("three"))) - .document(this.operationBuilder.request("http://localhost") - .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") - .build()); - } - - @Test - public void arrayRequestWithFields() throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`[]`", "`Array`", "one").row("`[]a.b`", "`Number`", "two") - .row("`[]a.c`", "`String`", "three") - .row("`[]a`", "`Object`", "four")); - - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"), - fieldWithPath("[]a.b").description("two"), - fieldWithPath("[]a.c").description("three"), - fieldWithPath("[]a").description("four"))) - .document(this.operationBuilder.request("http://localhost") - .content( - "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") - .build()); - } - - @Test - public void ignoredRequestField() throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", - "`Number`", "Field b")); - - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), - fieldWithPath("b").description("Field b"))) - .document(this.operationBuilder.request("http://localhost") - .content("{\"a\": 5, \"b\": 4}").build()); - } - - @Test - public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", - "`Number`", "Field b")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), - true).document( - this.operationBuilder.request("http://localhost") - .content("{\"a\": 5, \"b\": 4}").build()); - } - - @Test - public void missingOptionalRequestField() throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", - "`String`", "one")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") - .type(JsonFieldType.STRING).optional())) - .document(this.operationBuilder.request("http://localhost") - .content("{}").build()); - } - - @Test - public void missingIgnoredOptionalRequestFieldDoesNotRequireAType() + @RenderedSnippetTest + void entireSubsectionsCanBeDocumented(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet(Arrays.asList(subsectionWithPath("a").description("one"))) + .document(operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`Object`", "one")); + } + + @RenderedSnippetTest + void subsectionOfMapRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestFields(beneathPath("a"), fieldWithPath("b").description("one"), fieldWithPath("c").description("two")) + .document(operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + assertThat(snippets.requestFields("beneath-a")) + .isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one") + .row("`c`", "`String`", "two")); + } + + @RenderedSnippetTest + void subsectionOfMapRequestWithCommonPrefix(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + requestFields(beneathPath("a")).andWithPrefix("b.", fieldWithPath("c").description("two")) + .document(operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": {\"c\": \"charlie\"}}}") + .build()); + assertThat(snippets.requestFields("beneath-a")) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b.c`", "`String`", "two")); + } + + @RenderedSnippetTest + void arrayRequestWithFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("[]").description("one"), fieldWithPath("[]a.b").description("two"), + fieldWithPath("[]a.c").description("three"), fieldWithPath("[]a").description("four"))) + .document(operationBuilder.request("http://localhost") + .content("[{\"a\": {\"b\": 5, \"c\":\"charlie\"}}," + "{\"a\": {\"b\": 4, \"c\":\"chalk\"}}]") + .build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`[]`", "`Array`", "one") + .row("`[]a.b`", "`Number`", "two") + .row("`[]a.c`", "`String`", "three") + .row("`[]a`", "`Object`", "four")); + } + + @RenderedSnippetTest + void arrayRequestWithAlwaysNullField(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"))) + .document(operationBuilder.request("http://localhost") + .content("[{\"a\": {\"b\": null}}," + "{\"a\": {\"b\": null}}]") + .build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`[]a.b`", "`Null`", "one")); + } + + @RenderedSnippetTest + void subsectionOfArrayRequest(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + requestFields(beneathPath("[].a"), fieldWithPath("b").description("one"), fieldWithPath("c").description("two")) + .document(operationBuilder.request("http://localhost") + .content("[{\"a\": {\"b\": 5, \"c\": \"charlie\"}}]") + .build()); + assertThat(snippets.requestFields("beneath-[].a")) + .isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one") + .row("`c`", "`String`", "two")); + } + + @RenderedSnippetTest + void ignoredRequestField(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) + .document(operationBuilder.request("http://localhost").content("{\"a\": 5, \"b\": 4}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); + } + + @RenderedSnippetTest + void entireSubsectionCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description")); - new RequestFieldsSnippet(Arrays - .asList(fieldWithPath("a.b").description("one").ignored().optional())) - .document(this.operationBuilder.request("http://localhost") - .content("{}").build()); - } - - @Test - public void presentOptionalRequestField() throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", - "`String`", "one")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") - .type(JsonFieldType.STRING).optional())) - .document(this.operationBuilder.request("http://localhost") - .content("{\"a\": { \"b\": \"bravo\"}}").build()); - } - - @Test - public void requestFieldsWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")) - .willReturn(snippetResource("request-fields-with-title")); - this.snippet.expectRequestFields().withContents(containsString("Custom title")); + new RequestFieldsSnippet( + Arrays.asList(subsectionWithPath("a").ignored(), fieldWithPath("c").description("Field c"))) + .document(operationBuilder.request("http://localhost").content("{\"a\": {\"b\": 5}, \"c\": 4}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`c`", "`Number`", "Field c")); + } + @RenderedSnippetTest + void allUndocumentedRequestFieldsCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), true) + .document(operationBuilder.request("http://localhost").content("{\"a\": 5, \"b\": 4}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); + } + + @RenderedSnippetTest + void allUndocumentedFieldsContinueToBeIgnoredAfterAddingDescriptors(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), true) + .andWithPrefix("c.", fieldWithPath("d").description("Field d")) + .document( + operationBuilder.request("http://localhost").content("{\"a\":5,\"b\":4,\"c\":{\"d\": 3}}").build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "Field b") + .row("`c.d`", "`Number`", "Field d")); + } + + @RenderedSnippetTest + void missingOptionalRequestField(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a.b").description("one").type(JsonFieldType.STRING).optional())) + .document(operationBuilder.request("http://localhost").content("{}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); + } + + @RenderedSnippetTest + void missingIgnoredOptionalRequestFieldDoesNotRequireAType(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one").ignored().optional())) + .document(operationBuilder.request("http://localhost").content("{}").build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description")); + } + + @RenderedSnippetTest + void presentOptionalRequestField(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a.b").description("one").type(JsonFieldType.STRING).optional())) + .document(operationBuilder.request("http://localhost").content("{\"a\": { \"b\": \"bravo\"}}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-fields", template = "request-fields-with-title") + void requestFieldsWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes( - key("title").value("Custom title"))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .content("{\"a\": \"foo\"}").build()); - } - - @Test - public void requestFieldsWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")) - .willReturn(snippetResource("request-fields-with-extra-column")); - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description", "Foo") - .row("a.b", "Number", "one", "alpha") - .row("a.c", "String", "two", "bravo") - .row("a", "Object", "three", "charlie")); - - new RequestFieldsSnippet(Arrays.asList( - fieldWithPath("a.b").description("one") - .attributes(key("foo").value("alpha")), - fieldWithPath("a.c").description("two") - .attributes(key("foo").value("bravo")), - fieldWithPath("a").description("three") - .attributes(key("foo").value("charlie")))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .content( - "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") - .build()); - } - - @Test - public void fieldWithExplictExactlyMatchingType() throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", - "`Number`", "one")); - - new RequestFieldsSnippet(Arrays - .asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) - .document(this.operationBuilder.request("http://localhost") - .content("{\"a\": 5 }").build()); - } - - @Test - public void fieldWithExplictVariesType() throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", - "`Varies`", "one")); - - new RequestFieldsSnippet(Arrays - .asList(fieldWithPath("a").description("one").type(JsonFieldType.VARIES))) - .document(this.operationBuilder.request("http://localhost") - .content("{\"a\": 5 }").build()); - } - - @Test - public void xmlRequestFields() throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", - "`a`", "three")); + attributes(key("title").value("Custom title"))) + .document(operationBuilder.request("http://localhost").content("{\"a\": \"foo\"}").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withTitleAndHeader("Custom title", "Path", "Type", "Description") + .row("a", "String", "one")); + } + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-fields", template = "request-fields-with-extra-column") + void requestFieldsWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestFieldsSnippet( - Arrays.asList(fieldWithPath("a/b").description("one").type("b"), - fieldWithPath("a/c").description("two").type("c"), - fieldWithPath("a").description("three").type("a"))) - .document( - this.operationBuilder.request("http://localhost") - .content("5charlie") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void additionalDescriptors() throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") - .row("`a`", "`Object`", "three")); + Arrays.asList(fieldWithPath("a.b").description("one").attributes(key("foo").value("alpha")), + fieldWithPath("a.c").description("two").attributes(key("foo").value("bravo")), + fieldWithPath("a").description("three").attributes(key("foo").value("charlie")))) + .document(operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description", "Foo") + .row("a.b", "Number", "one", "alpha") + .row("a.c", "String", "two", "bravo") + .row("a", "Object", "three", "charlie")); + } - PayloadDocumentation - .requestFields(fieldWithPath("a.b").description("one"), - fieldWithPath("a.c").description("two")) - .and(fieldWithPath("a").description("three")) - .document(this.operationBuilder.request("http://localhost") - .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + @RenderedSnippetTest + void fieldWithExplicitExactlyMatchingType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) + .document(operationBuilder.request("http://localhost").content("{\"a\": 5 }").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); + } + + @RenderedSnippetTest + void fieldWithExplicitVariesType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.VARIES))) + .document(operationBuilder.request("http://localhost").content("{\"a\": 5 }").build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); + } + + @RenderedSnippetTest + void applicationXmlRequestFields(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + xmlRequestFields(MediaType.APPLICATION_XML, operationBuilder, snippets); + } + + @RenderedSnippetTest + void textXmlRequestFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + xmlRequestFields(MediaType.TEXT_XML, operationBuilder, snippets); + } + + @RenderedSnippetTest + void customXmlRequestFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + xmlRequestFields(MediaType.parseMediaType("application/vnd.com.example+xml"), operationBuilder, snippets); } - @Test - public void prefixedAdditionalDescriptors() throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") - .row("`a.c`", "`String`", "three")); + private void xmlRequestFields(MediaType contentType, OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one").type("b"), + fieldWithPath("a/c").description("two").type("c"), fieldWithPath("a").description("three").type("a"))) + .document(operationBuilder.request("http://localhost") + .content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, contentType.toString()) + .build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a/b`", "`b`", "one") + .row("`a/c`", "`c`", "two") + .row("`a`", "`a`", "three")); + } + + @RenderedSnippetTest + void entireSubsectionOfXmlPayloadCanBeDocumented(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet(Arrays.asList(subsectionWithPath("a").description("one").type("a"))) + .document(operationBuilder.request("http://localhost") + .content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); + } + + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + PayloadDocumentation + .requestFields(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two")) + .and(fieldWithPath("a").description("three")) + .document(operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one") + .row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); + } + @RenderedSnippetTest + void prefixedAdditionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { PayloadDocumentation.requestFields(fieldWithPath("a").description("one")) - .andWithPrefix("a.", fieldWithPath("b").description("two"), - fieldWithPath("c").description("three")) - .document(this.operationBuilder.request("http://localhost") - .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); - } - - @Test - public void requestWithFieldsWithEscapedContent() throws IOException { - this.snippet.expectRequestFields() - .withContents(tableWithHeader("Path", "Type", "Description").row( - escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), - escapeIfNecessary("three|four"))); - - new RequestFieldsSnippet(Arrays.asList( - fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) - .document(this.operationBuilder.request("http://localhost") - .content("{\"Foo|Bar\": 5}").build()); - } - - private String escapeIfNecessary(String input) { - if (this.templateFormat.equals(TemplateFormats.markdown())) { - return input; - } - return input.replace("|", "\\|"); + .andWithPrefix("a.", fieldWithPath("b").description("two"), fieldWithPath("c").description("three")) + .document(operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a`", "`Object`", "one") + .row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); + } + + @RenderedSnippetTest + void requestWithFieldsWithEscapedContent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) + .document(operationBuilder.request("http://localhost").content("{\"Foo|Bar\": 5}").build()); + assertThat(snippets.requestFields()).isTable( + (table) -> table.withHeader("Path", "Type", "Description").row("`Foo|Bar`", "`one|two`", "three|four")); + } + + @RenderedSnippetTest + void mapRequestWithVaryingKeysMatchedUsingWildcard(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("things.*.size").description("one"), + fieldWithPath("things.*.type").description("two"))) + .document(operationBuilder.request("http://localhost") + .content("{\"things\": {\"12abf\": {\"type\":" + "\"Whale\", \"size\": \"HUGE\"}," + + "\"gzM33\" : {\"type\": \"Screw\"," + "\"size\": \"SMALL\"}}}") + .build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`things.*.size`", "`String`", "one") + .row("`things.*.type`", "`String`", "two")); + } + + @RenderedSnippetTest + void requestWithArrayContainingFieldThatIsSometimesNull(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { + new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("assets[].name").description("one").type(JsonFieldType.STRING).optional())) + .document(operationBuilder.request("http://localhost") + .content("{\"assets\": [" + "{\"name\": \"sample1\"}, " + "{\"name\": null}, " + + "{\"name\": \"sample2\"}]}") + .build()); + assertThat(snippets.requestFields()).isTable( + (table) -> table.withHeader("Path", "Type", "Description").row("`assets[].name`", "`String`", "one")); + } + + @RenderedSnippetTest + void optionalFieldBeneathArrayThatIsSometimesAbsent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER).optional(), + fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) + .document(operationBuilder.request("http://localhost") + .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"c\": 2}, {\"b\": 1,\"c\": 2}]}") + .build()); + assertThat(snippets.requestFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a[].b`", "`Number`", "one") + .row("`a[].c`", "`Number`", "two")); + } + + @RenderedSnippetTest + void typeDeterminationDoesNotSetTypeOnDescriptor(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + FieldDescriptor descriptor = fieldWithPath("a.b").description("one"); + new RequestFieldsSnippet(Arrays.asList(descriptor)) + .document(operationBuilder.request("http://localhost").content("{\"a\": {\"b\": 5}}").build()); + assertThat(descriptor.getType()).isNull(); + assertThat(snippets.requestFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`Number`", "one")); + } + + @SnippetTest + void undocumentedRequestField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Collections.emptyList()) + .document(operationBuilder.request("http://localhost").content("{\"a\": 5}").build())) + .withMessageStartingWith("The following parts of the payload were not documented:"); + } + + @SnippetTest + void missingRequestField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(operationBuilder.request("http://localhost").content("{}").build())) + .withMessage("Fields with the following paths were not found in the payload: [a.b]"); + } + + @SnippetTest + void missingOptionalRequestFieldWithNoTypeProvided(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypeRequiredException.class).isThrownBy( + () -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one").optional())) + .document(operationBuilder.request("http://localhost").content("{ }").build())); + } + + @SnippetTest + void undocumentedRequestFieldAndMissingRequestField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(operationBuilder.request("http://localhost").content("{ \"a\": { \"c\": 5 }}").build())) + .withMessageStartingWith("The following parts of the payload were not documented:") + .withMessageEndingWith("Fields with the following paths were not found in the payload: [a.b]"); + } + + @SnippetTest + void attemptToDocumentFieldsWithNoRequestBody(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(operationBuilder.request("http://localhost").build())) + .withMessage("Cannot document request fields as the request body is empty"); + } + + @SnippetTest + void fieldWithExplicitTypeThatDoesNotMatchThePayload(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypesDoNotMatchException.class) + .isThrownBy(() -> new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) + .document(operationBuilder.request("http://localhost").content("{ \"a\": 5 }").build())) + .withMessage("The documented type of the field 'a' is Object but the actual type is Number"); + } + + @SnippetTest + void fieldWithExplicitSpecificTypeThatActuallyVaries(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypesDoNotMatchException.class).isThrownBy(() -> new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) + .document(operationBuilder.request("http://localhost").content("[{ \"a\": 5 },{ \"a\": \"b\" }]").build())) + .withMessage("The documented type of the field '[].a' is Object but the actual type is Varies"); + } + + @SnippetTest + void undocumentedXmlRequestField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Collections.emptyList()) + .document(operationBuilder.request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessageStartingWith("The following parts of the payload were not documented:"); + } + + @SnippetTest + void xmlDescendentsAreNotDocumentedByFieldDescriptor(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").type("a").description("one"))) + .document(operationBuilder.request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessageStartingWith("The following parts of the payload were not documented:"); + } + + @SnippetTest + void xmlRequestFieldWithNoType(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypeRequiredException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(operationBuilder.request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())); + } + + @SnippetTest + void missingXmlRequestField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a/b").description("one"), fieldWithPath("a").description("one"))) + .document(operationBuilder.request("http://localhost") + .content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessage("Fields with the following paths were not found in the payload: [a/b]"); + } + + @SnippetTest + void undocumentedXmlRequestFieldAndMissingXmlRequestField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) + .document(operationBuilder.request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessageStartingWith("The following parts of the payload were not documented:") + .withMessageEndingWith("Fields with the following paths were not found in the payload: [a/b]"); + } + + @SnippetTest + void unsupportedContent(OperationBuilder operationBuilder) { + assertThatExceptionOfType(PayloadHandlingException.class) + .isThrownBy(() -> new RequestFieldsSnippet(Collections.emptyList()) + .document(operationBuilder.request("http://localhost") + .content("Some plain text") + .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE) + .build())) + .withMessage("Cannot handle text/plain content as it could not be parsed as JSON or XML"); + } + + @SnippetTest + void nonOptionalFieldBeneathArrayThatIsSometimesNull(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER), + fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) + .document(operationBuilder.request("http://localhost") + .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"b\": null, \"c\": 2}," + " {\"b\": 1,\"c\": 2}]}") + .build())) + .withMessageStartingWith("Fields with the following paths were not found in the payload: [a[].b]"); + } + + @SnippetTest + void nonOptionalFieldBeneathArrayThatIsSometimesAbsent(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER), + fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) + .document(operationBuilder.request("http://localhost") + .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"c\": 2}, {\"b\": 1,\"c\": 2}]}") + .build())) + .withMessageStartingWith("Fields with the following paths were not found in the payload: [a[].b]"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java new file mode 100644 index 000000000..f4924d752 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for {@link RequestPartFieldsSnippet}. + * + * @author Mathieu Pousse + * @author Andy Wilkinson + */ +public class RequestPartFieldsSnippetTests { + + @RenderedSnippetTest + void mapRequestPartFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestPartFieldsSnippet("one", + Arrays.asList(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), + fieldWithPath("a").description("three"))) + .document(operationBuilder.request("http://localhost") + .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + assertThat(snippets.requestPartFields("one")).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one") + .row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); + } + + @RenderedSnippetTest + void mapRequestPartSubsectionFields(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestPartFieldsSnippet("one", beneathPath("a"), + Arrays.asList(fieldWithPath("b").description("one"), fieldWithPath("c").description("two"))) + .document(operationBuilder.request("http://localhost") + .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + assertThat(snippets.requestPartFields("one", "beneath-a")) + .isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one") + .row("`c`", "`String`", "two")); + } + + @RenderedSnippetTest + void multipleRequestParts(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + Operation operation = operationBuilder.request("http://localhost") + .part("one", "{}".getBytes()) + .and() + .part("two", "{}".getBytes()) + .build(); + new RequestPartFieldsSnippet("one", Collections.emptyList()).document(operation); + new RequestPartFieldsSnippet("two", Collections.emptyList()).document(operation); + assertThat(snippets.requestPartFields("one")).isNotNull(); + assertThat(snippets.requestPartFields("two")).isNotNull(); + } + + @RenderedSnippetTest + void allUndocumentedRequestPartFieldsCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestPartFieldsSnippet("one", Arrays.asList(fieldWithPath("b").description("Field b")), true).document( + operationBuilder.request("http://localhost").part("one", "{\"a\": 5, \"b\": 4}".getBytes()).build()); + assertThat(snippets.requestPartFields("one")) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); + } + + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + PayloadDocumentation + .requestPartFields("one", fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two")) + .and(fieldWithPath("a").description("three")) + .document(operationBuilder.request("http://localhost") + .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + assertThat(snippets.requestPartFields("one")).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one") + .row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); + } + + @RenderedSnippetTest + void prefixedAdditionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + PayloadDocumentation.requestPartFields("one", fieldWithPath("a").description("one")) + .andWithPrefix("a.", fieldWithPath("b").description("two"), fieldWithPath("c").description("three")) + .document(operationBuilder.request("http://localhost") + .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + assertThat(snippets.requestPartFields("one")).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a`", "`Object`", "one") + .row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); + } + + @SnippetTest + void undocumentedRequestPartField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestPartFieldsSnippet("part", Collections.emptyList()) + .document(operationBuilder.request("http://localhost").part("part", "{\"a\": 5}".getBytes()).build())) + .withMessageStartingWith("The following parts of the payload were not documented:"); + } + + @SnippetTest + void missingRequestPartField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestPartFieldsSnippet("part", Arrays.asList(fieldWithPath("b").description("one"))) + .document(operationBuilder.request("http://localhost").part("part", "{\"a\": 5}".getBytes()).build())) + .withMessageStartingWith("The following parts of the payload were not documented:"); + } + + @SnippetTest + void missingRequestPart(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class).isThrownBy( + () -> new RequestPartFieldsSnippet("another", Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(operationBuilder.request("http://localhost") + .part("part", "{\"a\": {\"b\": 5}}".getBytes()) + .build())) + .withMessage("A request part named 'another' was not found in the request"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java new file mode 100644 index 000000000..74c659954 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link ResponseBodySnippet}. + * + * @author Andy Wilkinson + */ +class ResponseBodySnippetTests { + + @RenderedSnippetTest + void responseWithBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseBodySnippet().document(operationBuilder.response().content("some content").build()); + assertThat(snippets.responseBody()) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("some content")); + } + + @RenderedSnippetTest + void responseWithNoBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseBodySnippet().document(operationBuilder.response().build()); + assertThat(snippets.responseBody()).isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("")); + } + + @RenderedSnippetTest + void responseWithJsonMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseBodySnippet().document( + operationBuilder.response().header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).build()); + assertThat(snippets.responseBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("")); + } + + @RenderedSnippetTest + void responseWithJsonSubtypeMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseBodySnippet().document(operationBuilder.response() + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PROBLEM_JSON_VALUE) + .build()); + assertThat(snippets.responseBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("")); + } + + @RenderedSnippetTest + void responseWithXmlMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseBodySnippet().document( + operationBuilder.response().header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE).build()); + assertThat(snippets.responseBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("xml", "nowrap").content("")); + } + + @RenderedSnippetTest + void responseWithXmlSubtypeMediaType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseBodySnippet().document(operationBuilder.response() + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_ATOM_XML_VALUE) + .build()); + assertThat(snippets.responseBody()) + .isCodeBlock((codeBlock) -> codeBlock.withLanguageAndOptions("xml", "nowrap").content("")); + } + + @RenderedSnippetTest + void subsectionOfResponseBody(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + responseBody(beneathPath("a.b")) + .document(operationBuilder.response().content("{\"a\":{\"b\":{\"c\":5}}}").build()); + assertThat(snippets.responseBody("beneath-a.b")) + .isCodeBlock((codeBlock) -> codeBlock.withOptions("nowrap").content("{\"c\":5}")); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-body", template = "response-body-with-language") + void customSnippetAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseBodySnippet(attributes(key("language").value("json"))) + .document(operationBuilder.response().content("{\"a\":\"alpha\"}").build()); + assertThat(snippets.responseBody()).isCodeBlock( + (codeBlock) -> codeBlock.withLanguageAndOptions("json", "nowrap").content("{\"a\":\"alpha\"}")); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java deleted file mode 100644 index 953ee41f0..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.payload; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; - -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; - -/** - * Tests for failures when rendering {@link ResponseFieldsSnippet} due to missing or - * undocumented fields. - * - * @author Andy Wilkinson - */ -public class ResponseFieldsSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void attemptToDocumentFieldsWithNoResponseBody() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage( - equalTo("Cannot document response fields as the response body is empty")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(this.operationBuilder.build()); - } - - @Test - public void fieldWithExplicitTypeThatDoesNotMatchThePayload() throws IOException { - this.thrown.expect(FieldTypesDoNotMatchException.class); - this.thrown.expectMessage(equalTo("The documented type of the field 'a' is" - + " Object but the actual type is Number")); - new ResponseFieldsSnippet(Arrays - .asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) - .document(this.operationBuilder.response() - .content("{ \"a\": 5 }}").build()); - } - - @Test - public void fieldWithExplicitSpecificTypeThatActuallyVaries() throws IOException { - this.thrown.expect(FieldTypesDoNotMatchException.class); - this.thrown.expectMessage(equalTo("The documented type of the field '[].a' is" - + " Object but the actual type is Varies")); - new ResponseFieldsSnippet(Arrays.asList( - fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) - .document(this.operationBuilder.response() - .content("[{ \"a\": 5 },{ \"a\": \"b\" }]").build()); - } - - @Test - public void undocumentedXmlResponseField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(startsWith( - "The following parts of the payload were not" + " documented:")); - new ResponseFieldsSnippet(Collections.emptyList()) - .document(this.operationBuilder.response().content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void missingXmlAttribute() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/@id]")); - new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a").description("one").type("b"), - fieldWithPath("a/@id").description("two").type("c"))) - .document( - this.operationBuilder.response() - .content("foo") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void documentedXmlAttributesAreRemoved() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo( - String.format("The following parts of the payload were not documented:" - + "%nbar%n"))); - new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a/@id").description("one").type("a"))) - .document(this.operationBuilder.response() - .content("bar") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void xmlResponseFieldWithNoType() throws IOException { - this.thrown.expect(FieldTypeRequiredException.class); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(this.operationBuilder.response().content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void missingXmlResponseField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/b]")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))).document(this.operationBuilder - .response().content("") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void undocumentedXmlResponseFieldAndMissingXmlResponseField() - throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(startsWith( - "The following parts of the payload were not" + " documented:")); - this.thrown - .expectMessage(endsWith("Fields with the following paths were not found" - + " in the payload: [a/b]")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) - .document(this.operationBuilder.response().content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index dedd20d66..e238851b9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,22 +18,22 @@ import java.io.IOException; import java.util.Arrays; - -import org.junit.Test; +import java.util.Collections; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -41,304 +41,488 @@ * Tests for {@link ResponseFieldsSnippet}. * * @author Andy Wilkinson + * @author Sungjun Lee */ -public class ResponseFieldsSnippetTests extends AbstractSnippetTests { - - public ResponseFieldsSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +public class ResponseFieldsSnippetTests { - @Test - public void mapResponseWithFields() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") - .row("`assets`", "`Array`", "three") - .row("`assets[]`", "`Array`", "four") - .row("`assets[].id`", "`Number`", "five") - .row("`assets[].name`", "`String`", "six")); + @RenderedSnippetTest + void mapResponseWithFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"), - fieldWithPath("date").description("two"), - fieldWithPath("assets").description("three"), - fieldWithPath("assets[]").description("four"), - fieldWithPath("assets[].id").description("five"), + fieldWithPath("date").description("two"), fieldWithPath("assets").description("three"), + fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), fieldWithPath("assets[].name").description("six"))) - .document(this.operationBuilder.response() - .content( - "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" - + " [{\"id\":356,\"name\": \"sample\"}]}") - .build()); - } - - @Test - public void arrayResponseWithFields() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`[]a.b`", "`Number`", "one") - .row("`[]a.c`", "`String`", "two") - .row("`[]a`", "`Object`", "three")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), - fieldWithPath("[]a.c").description("two"), - fieldWithPath("[]a").description("three"))) - .document(this.operationBuilder.response() - .content( - "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") - .build()); - } - - @Test - public void arrayResponse() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`[]`", - "`Array`", "one")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) - .document(this.operationBuilder.response() - .content("[\"a\", \"b\", \"c\"]").build()); + .document(operationBuilder.response() + .content("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + " [{\"id\":356,\"name\": \"sample\"}]}") + .build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`id`", "`Number`", "one") + .row("`date`", "`String`", "two") + .row("`assets`", "`Array`", "three") + .row("`assets[]`", "`Array`", "four") + .row("`assets[].id`", "`Number`", "five") + .row("`assets[].name`", "`String`", "six")); + } + + @RenderedSnippetTest + void mapResponseWithNullField(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(operationBuilder.response().content("{\"a\": {\"b\": null}}").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`Null`", "one")); + } + + @RenderedSnippetTest + void subsectionOfMapResponse(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + responseFields(beneathPath("a"), fieldWithPath("b").description("one"), fieldWithPath("c").description("two")) + .document(operationBuilder.response().content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + assertThat(snippets.responseFields("beneath-a")) + .isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one") + .row("`c`", "`String`", "two")); + } + + @RenderedSnippetTest + void subsectionOfMapResponseBeneathAnArray(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + responseFields(beneathPath("a.b.[]"), fieldWithPath("c").description("one"), + fieldWithPath("d.[].e").description("two")) + .document(operationBuilder.response() + .content("{\"a\": {\"b\": [{\"c\": 1, \"d\": [{\"e\": 5}]}, {\"c\": 3, \"d\": [{\"e\": 4}]}]}}") + .build()); + assertThat(snippets.responseFields("beneath-a.b.[]")) + .isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`c`", "`Number`", "one") + .row("`d.[].e`", "`Number`", "two")); + } + + @RenderedSnippetTest + void subsectionOfMapResponseWithCommonsPrefix(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + responseFields(beneathPath("a")).andWithPrefix("b.", fieldWithPath("c").description("two")) + .document(operationBuilder.response().content("{\"a\": {\"b\": {\"c\": \"charlie\"}}}").build()); + assertThat(snippets.responseFields("beneath-a")) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b.c`", "`String`", "two")); } - @Test - public void ignoredResponseField() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", - "`Number`", "Field b")); + @RenderedSnippetTest + void arrayResponseWithFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), + fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a").description("three"))) + .document(operationBuilder.response() + .content("[{\"a\": {\"b\": 5, \"c\":\"charlie\"}}," + "{\"a\": {\"b\": 4, \"c\":\"chalk\"}}]") + .build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`[]a.b`", "`Number`", "one") + .row("`[]a.c`", "`String`", "two") + .row("`[]a`", "`Object`", "three")); + } - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), - fieldWithPath("b").description("Field b"))) - .document(this.operationBuilder.response() - .content("{\"a\": 5, \"b\": 4}").build()); + @RenderedSnippetTest + void arrayResponseWithAlwaysNullField(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"))).document( + operationBuilder.response().content("[{\"a\": {\"b\": null}}," + "{\"a\": {\"b\": null}}]").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`[]a.b`", "`Null`", "one")); } - @Test - public void allUndocumentedFieldsCanBeIgnored() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", - "`Number`", "Field b")); + @RenderedSnippetTest + void arrayResponse(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) + .document(operationBuilder.response().content("[\"a\", \"b\", \"c\"]").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`[]`", "`Array`", "one")); + } + @RenderedSnippetTest + void ignoredResponseField(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("b").description("Field b")), true) - .document(this.operationBuilder.response() - .content("{\"a\": 5, \"b\": 4}").build()); + Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) + .document(operationBuilder.response().content("{\"a\": 5, \"b\": 4}").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); } - @Test - public void responseFieldsWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-fields")) - .willReturn(snippetResource("response-fields-with-title")); - this.snippet.expectResponseFields().withContents(containsString("Custom title")); + @RenderedSnippetTest + void allUndocumentedFieldsCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), true) + .document(operationBuilder.response().content("{\"a\": 5, \"b\": 4}").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); + } + + @RenderedSnippetTest + void allUndocumentedFieldsContinueToBeIgnoredAfterAddingDescriptors(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), true) + .andWithPrefix("c.", fieldWithPath("d").description("Field d")) + .document(operationBuilder.response().content("{\"a\":5,\"b\":4,\"c\":{\"d\": 3}}").build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "Field b") + .row("`c.d`", "`Number`", "Field d")); + } + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-fields", template = "response-fields-with-title") + void responseFieldsWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes( - key("title").value("Custom title"))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .response().content("{\"a\": \"foo\"}") - .build()); - } - - @Test - public void missingOptionalResponseField() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", - "`String`", "one")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") - .type(JsonFieldType.STRING).optional())) - .document(this.operationBuilder.response().content("{}").build()); - } - - @Test - public void missingIgnoredOptionalResponseFieldDoesNotRequireAType() + attributes(key("title").value("Custom title"))) + .document(operationBuilder.response().content("{\"a\": \"foo\"}").build()); + assertThat(snippets.responseFields()).contains("Custom title"); + } + + @RenderedSnippetTest + void missingOptionalResponseField(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description")); - new ResponseFieldsSnippet(Arrays - .asList(fieldWithPath("a.b").description("one").ignored().optional())) - .document(this.operationBuilder.response().content("{}").build()); - } - - @Test - public void presentOptionalResponseField() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", - "`String`", "one")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") - .type(JsonFieldType.STRING).optional())) - .document(this.operationBuilder.response() - .content("{\"a\": { \"b\": \"bravo\"}}").build()); - } - - @Test - public void responseFieldsWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-fields")) - .willReturn(snippetResource("response-fields-with-extra-column")); - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description", "Foo") - .row("a.b", "Number", "one", "alpha") - .row("a.c", "String", "two", "bravo") - .row("a", "Object", "three", "charlie")); - - new ResponseFieldsSnippet(Arrays.asList( - fieldWithPath("a.b").description("one") - .attributes(key("foo").value("alpha")), - fieldWithPath("a.c").description("two") - .attributes(key("foo").value("bravo")), - fieldWithPath("a").description("three") - .attributes(key("foo").value("charlie")))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .response() - .content( - "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") - .build()); - } - - @Test - public void fieldWithExplictExactlyMatchingType() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", - "`Number`", "one")); - - new ResponseFieldsSnippet(Arrays - .asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) - .document(this.operationBuilder.response().content("{\"a\": 5 }") - .build()); - } - - @Test - public void fieldWithExplictVariesType() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", - "`Varies`", "one")); - - new ResponseFieldsSnippet(Arrays - .asList(fieldWithPath("a").description("one").type(JsonFieldType.VARIES))) - .document(this.operationBuilder.response().content("{\"a\": 5 }") - .build()); - } - - @Test - public void xmlResponseFields() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", - "`a`", "three")); - new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a/b").description("one").type("b"), - fieldWithPath("a/c").description("two").type("c"), - fieldWithPath("a").description("three").type("a"))) - .document( - this.operationBuilder.response() - .content("5charlie") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void xmlAttribute() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a").description("one").type("b"), - fieldWithPath("a/@id").description("two").type("c"))) - .document( - this.operationBuilder.response() - .content("foo") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void missingOptionalXmlAttribute() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); + Arrays.asList(fieldWithPath("a.b").description("one").type(JsonFieldType.STRING).optional())) + .document(operationBuilder.response().content("{}").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); + } + + @RenderedSnippetTest + void missingIgnoredOptionalResponseFieldDoesNotRequireAType(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one").ignored().optional())) + .document(operationBuilder.response().content("{}").build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description")); + } + + @RenderedSnippetTest + void presentOptionalResponseField(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a").description("one").type("b"), - fieldWithPath("a/@id").description("two").type("c").optional())) - .document( - this.operationBuilder.response() - .content("foo") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void undocumentedAttributeDoesNotCauseFailure() throws IOException { - this.snippet.expectResponseFields().withContents( - tableWithHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); + Arrays.asList(fieldWithPath("a.b").description("one").type(JsonFieldType.STRING).optional())) + .document(operationBuilder.response().content("{\"a\": { \"b\": \"bravo\"}}").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "response-fields", template = "response-fields-with-extra-column") + void responseFieldsWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a").description("one").type("a"))).document( - this.operationBuilder.response().content("bar") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void additionalDescriptors() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") - .row("`assets`", "`Array`", "three") - .row("`assets[]`", "`Array`", "four") - .row("`assets[].id`", "`Number`", "five") - .row("`assets[].name`", "`String`", "six")); + Arrays.asList(fieldWithPath("a.b").description("one").attributes(key("foo").value("alpha")), + fieldWithPath("a.c").description("two").attributes(key("foo").value("bravo")), + fieldWithPath("a").description("three").attributes(key("foo").value("charlie")))) + .document(operationBuilder.response().content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description", "Foo") + .row("a.b", "Number", "one", "alpha") + .row("a.c", "String", "two", "bravo") + .row("a", "Object", "three", "charlie")); + } + + @RenderedSnippetTest + void fieldWithExplicitExactlyMatchingType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) + .document(operationBuilder.response().content("{\"a\": 5 }").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); + } + + @RenderedSnippetTest + void fieldWithExplicitVariesType(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.VARIES))) + .document(operationBuilder.response().content("{\"a\": 5 }").build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); + } + + @RenderedSnippetTest + void applicationXmlResponseFields(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + xmlResponseFields(MediaType.APPLICATION_XML, operationBuilder, snippets); + } + + @RenderedSnippetTest + void textXmlResponseFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + xmlResponseFields(MediaType.TEXT_XML, operationBuilder, snippets); + } + + @RenderedSnippetTest + void customXmlResponseFields(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + xmlResponseFields(MediaType.parseMediaType("application/vnd.com.example+xml"), operationBuilder, snippets); + } + + private void xmlResponseFields(MediaType contentType, OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one").type("b"), + fieldWithPath("a/c").description("two").type("c"), fieldWithPath("a").description("three").type("a"))) + .document(operationBuilder.response() + .content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, contentType.toString()) + .build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a/b`", "`b`", "one") + .row("`a/c`", "`c`", "two") + .row("`a`", "`a`", "three")); + } + + @RenderedSnippetTest + void xmlAttribute(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type("b"), + fieldWithPath("a/@id").description("two").type("c"))) + .document(operationBuilder.response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a`", "`b`", "one") + .row("`a/@id`", "`c`", "two")); + } + + @RenderedSnippetTest + void missingOptionalXmlAttribute(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type("b"), + fieldWithPath("a/@id").description("two").type("c").optional())) + .document(operationBuilder.response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a`", "`b`", "one") + .row("`a/@id`", "`c`", "two")); + } + + @RenderedSnippetTest + void undocumentedAttributeDoesNotCauseFailure(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type("a"))) + .document(operationBuilder.response() + .content("bar") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); + } + + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { PayloadDocumentation - .responseFields(fieldWithPath("id").description("one"), - fieldWithPath("date").description("two"), - fieldWithPath("assets").description("three")) - .and(fieldWithPath("assets[]").description("four"), - fieldWithPath("assets[].id").description("five"), - fieldWithPath("assets[].name").description("six")) - .document(this.operationBuilder.response() - .content("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" - + " [{\"id\":356,\"name\": \"sample\"}]}") - .build()); - } - - @Test - public void prefixedAdditionalDescriptors() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") - .row("`a.c`", "`String`", "three")); + .responseFields(fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), + fieldWithPath("assets").description("three")) + .and(fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), + fieldWithPath("assets[].name").description("six")) + .document(operationBuilder.response() + .content("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + " [{\"id\":356,\"name\": \"sample\"}]}") + .build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`id`", "`Number`", "one") + .row("`date`", "`String`", "two") + .row("`assets`", "`Array`", "three") + .row("`assets[]`", "`Array`", "four") + .row("`assets[].id`", "`Number`", "five") + .row("`assets[].name`", "`String`", "six")); + } + @RenderedSnippetTest + void prefixedAdditionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { PayloadDocumentation.responseFields(fieldWithPath("a").description("one")) - .andWithPrefix("a.", fieldWithPath("b").description("two"), - fieldWithPath("c").description("three")) - .document(this.operationBuilder.response() - .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); - } - - @Test - public void responseWithFieldsWithEscapedContent() throws IOException { - this.snippet.expectResponseFields() - .withContents(tableWithHeader("Path", "Type", "Description").row( - escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), - escapeIfNecessary("three|four"))); - - new ResponseFieldsSnippet(Arrays.asList( - fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) - .document(this.operationBuilder.response() - .content("{\"Foo|Bar\": 5}").build()); - } - - private String escapeIfNecessary(String input) { - if (this.templateFormat.equals(TemplateFormats.markdown())) { - return input; - } - return input.replace("|", "\\|"); + .andWithPrefix("a.", fieldWithPath("b").description("two"), fieldWithPath("c").description("three")) + .document(operationBuilder.response().content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a`", "`Object`", "one") + .row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); + } + + @RenderedSnippetTest + void responseWithFieldsWithEscapedContent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) + .document(operationBuilder.response().content("{\"Foo|Bar\": 5}").build()); + assertThat(snippets.responseFields()).isTable( + (table) -> table.withHeader("Path", "Type", "Description").row("`Foo|Bar`", "`one|two`", "three|four")); + } + + @RenderedSnippetTest + void mapResponseWithVaryingKeysMatchedUsingWildcard(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("things.*.size").description("one"), + fieldWithPath("things.*.type").description("two"))) + .document(operationBuilder.response() + .content("{\"things\": {\"12abf\": {\"type\":" + "\"Whale\", \"size\": \"HUGE\"}," + + "\"gzM33\" : {\"type\": \"Screw\"," + "\"size\": \"SMALL\"}}}") + .build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`things.*.size`", "`String`", "one") + .row("`things.*.type`", "`String`", "two")); + } + + @RenderedSnippetTest + void responseWithArrayContainingFieldThatIsSometimesNull(OperationBuilder operationBuilder, + AssertableSnippets snippets) throws IOException { + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("assets[].name").description("one").type(JsonFieldType.STRING).optional())) + .document(operationBuilder.response() + .content("{\"assets\": [" + "{\"name\": \"sample1\"}, " + "{\"name\": null}, " + + "{\"name\": \"sample2\"}]}") + .build()); + assertThat(snippets.responseFields()).isTable( + (table) -> table.withHeader("Path", "Type", "Description").row("`assets[].name`", "`String`", "one")); + } + + @RenderedSnippetTest + void optionalFieldBeneathArrayThatIsSometimesAbsent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER).optional(), + fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) + .document(operationBuilder.response() + .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"c\": 2}, {\"b\": 1,\"c\": 2}]}") + .build()); + assertThat(snippets.responseFields()).isTable((table) -> table.withHeader("Path", "Type", "Description") + .row("`a[].b`", "`Number`", "one") + .row("`a[].c`", "`Number`", "two")); + } + + @RenderedSnippetTest + void typeDeterminationDoesNotSetTypeOnDescriptor(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + FieldDescriptor descriptor = fieldWithPath("id").description("one"); + new ResponseFieldsSnippet(Arrays.asList(descriptor)) + .document(operationBuilder.response().content("{\"id\": 67}").build()); + assertThat(descriptor.getType()).isNull(); + assertThat(snippets.responseFields()) + .isTable((table) -> table.withHeader("Path", "Type", "Description").row("`id`", "`Number`", "one")); + } + + @SnippetTest + void attemptToDocumentFieldsWithNoResponseBody(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(operationBuilder.build())) + .withMessage("Cannot document response fields as the response body is empty"); + } + + @SnippetTest + void fieldWithExplicitTypeThatDoesNotMatchThePayload(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypesDoNotMatchException.class) + .isThrownBy(() -> new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) + .document(operationBuilder.response().content("{ \"a\": 5 }}").build())) + .withMessage("The documented type of the field 'a' is Object but the actual type is Number"); + } + + @SnippetTest + void fieldWithExplicitSpecificTypeThatActuallyVaries(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypesDoNotMatchException.class) + .isThrownBy(() -> new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) + .document(operationBuilder.response().content("[{ \"a\": 5 },{ \"a\": \"b\" }]").build())) + .withMessage("The documented type of the field '[].a' is Object but the actual type is Varies"); + } + + @SnippetTest + void undocumentedXmlResponseField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet(Collections.emptyList()) + .document(operationBuilder.response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessageStartingWith("The following parts of the payload were not documented:"); + } + + @SnippetTest + void missingXmlAttribute(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one").type("b"), + fieldWithPath("a/@id").description("two").type("c"))) + .document(operationBuilder.response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessage("Fields with the following paths were not found in the payload: [a/@id]"); + } + + @SnippetTest + void documentedXmlAttributesAreRemoved(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy( + () -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/@id").description("one").type("a"))) + .document(operationBuilder.response() + .content("bar") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessage(String.format("The following parts of the payload were not documented:%nbar%n")); + } + + @SnippetTest + void xmlResponseFieldWithNoType(OperationBuilder operationBuilder) { + assertThatExceptionOfType(FieldTypeRequiredException.class) + .isThrownBy(() -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(operationBuilder.response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())); + } + + @SnippetTest + void missingXmlResponseField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a/b").description("one"), fieldWithPath("a").description("one"))) + .document(operationBuilder.response() + .content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessage("Fields with the following paths were not found in the payload: [a/b]"); + } + + @SnippetTest + void undocumentedXmlResponseFieldAndMissingXmlResponseField(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) + .document(operationBuilder.response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build())) + .withMessageStartingWith("The following parts of the payload were not documented:") + .withMessageEndingWith("Fields with the following paths were not found in the payload: [a/b]"); + } + + @SnippetTest + void unsupportedContent(OperationBuilder operationBuilder) { + assertThatExceptionOfType(PayloadHandlingException.class) + .isThrownBy(() -> new ResponseFieldsSnippet(Collections.emptyList()) + .document(operationBuilder.response() + .content("Some plain text") + .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE) + .build())) + .withMessage("Cannot handle text/plain content as it could not be parsed as JSON or XML"); + } + + @SnippetTest + void nonOptionalFieldBeneathArrayThatIsSometimesNull(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER), + fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) + .document(operationBuilder.response() + .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"b\": null, \"c\": 2}," + " {\"b\": 1,\"c\": 2}]}") + .build())) + .withMessageStartingWith("Fields with the following paths were not found in the payload: [a[].b]"); + } + + @SnippetTest + void nonOptionalFieldBeneathArrayThatIsSometimesAbsent(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a[].b").description("one").type(JsonFieldType.NUMBER), + fieldWithPath("a[].c").description("two").type(JsonFieldType.NUMBER))) + .document(operationBuilder.response() + .content("{\"a\":[{\"b\": 1,\"c\": 2}, " + "{\"c\": 2}, {\"b\": 1,\"c\": 2}]}") + .build())) + .withMessageStartingWith("Fields with the following paths were not found in the payload: [a[].b]"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java new file mode 100644 index 000000000..6b98dac40 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.payload; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; + +/** + * Tests for {@link XmlContentHandler}. + * + * @author Andy Wilkinson + */ +public class XmlContentHandlerTests { + + @Test + public void topLevelElementCanBeDocumented() { + List descriptors = Arrays.asList(fieldWithPath("a").type("a").description("description")); + String undocumentedContent = createHandler("5", descriptors).getUndocumentedContent(); + assertThat(undocumentedContent).isNull(); + } + + @Test + public void nestedElementCanBeDocumentedLeavingAncestors() { + List descriptors = Arrays.asList(fieldWithPath("a/b").type("b").description("description")); + String undocumentedContent = createHandler("5", descriptors).getUndocumentedContent(); + assertThat(undocumentedContent).isEqualTo(String.format("%n")); + } + + @Test + public void fieldDescriptorDoesNotDocumentEntireSubsection() { + List descriptors = Arrays.asList(fieldWithPath("a").type("a").description("description")); + String undocumentedContent = createHandler("5", descriptors).getUndocumentedContent(); + assertThat(undocumentedContent).isEqualTo(String.format("%n 5%n%n")); + } + + @Test + public void subsectionDescriptorDocumentsEntireSubsection() { + List descriptors = Arrays.asList(subsectionWithPath("a").type("a").description("description")); + String undocumentedContent = createHandler("5", descriptors).getUndocumentedContent(); + assertThat(undocumentedContent).isNull(); + } + + @Test + public void multipleElementsCanBeInDescendingOrderDocumented() { + List descriptors = Arrays.asList(fieldWithPath("a").type("a").description("description"), + fieldWithPath("a/b").type("b").description("description")); + String undocumentedContent = createHandler("5", descriptors).getUndocumentedContent(); + assertThat(undocumentedContent).isNull(); + } + + @Test + public void multipleElementsCanBeInAscendingOrderDocumented() { + List descriptors = Arrays.asList(fieldWithPath("a/b").type("b").description("description"), + fieldWithPath("a").type("a").description("description")); + String undocumentedContent = createHandler("5", descriptors).getUndocumentedContent(); + assertThat(undocumentedContent).isNull(); + } + + @Test + public void failsFastWithNonXmlContent() { + assertThatExceptionOfType(PayloadHandlingException.class) + .isThrownBy(() -> createHandler("non-XML content", Collections.emptyList())); + } + + private XmlContentHandler createHandler(String xml, List descriptors) { + return new XmlContentHandler(xml.getBytes(), descriptors); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetTests.java new file mode 100644 index 000000000..0cf383690 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/FormParametersSnippetTests.java @@ -0,0 +1,188 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.request; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link FormParametersSnippet}. + * + * @author Andy Wilkinson + */ +class FormParametersSnippetTests { + + @RenderedSnippetTest + void formParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new FormParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) + .document(operationBuilder.request("http://localhost").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + } + + @RenderedSnippetTest + void formParameterWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost").content("a=").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one")); + } + + @RenderedSnippetTest + void ignoredFormParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new FormParametersSnippet( + Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) + .document(operationBuilder.request("http://localhost").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`b`", "two")); + } + + @RenderedSnippetTest + void allUndocumentedFormParametersCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new FormParametersSnippet(Arrays.asList(parameterWithName("b").description("two")), true) + .document(operationBuilder.request("http://localhost").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`b`", "two")); + } + + @RenderedSnippetTest + void missingOptionalFormParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional(), + parameterWithName("b").description("two"))) + .document(operationBuilder.request("http://localhost").content("b=bravo").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + } + + @RenderedSnippetTest + void presentOptionalFormParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional())) + .document(operationBuilder.request("http://localhost").content("a=alpha").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one")); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "form-parameters", template = "form-parameters-with-title") + void formParametersWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new FormParametersSnippet( + Arrays.asList(parameterWithName("a").description("one").attributes(key("foo").value("alpha")), + parameterWithName("b").description("two").attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))) + .document(operationBuilder.request("http://localhost").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()).contains("The title"); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "form-parameters", template = "form-parameters-with-extra-column") + void formParametersWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new FormParametersSnippet( + Arrays.asList(parameterWithName("a").description("one").attributes(key("foo").value("alpha")), + parameterWithName("b").description("two").attributes(key("foo").value("bravo")))) + .document(operationBuilder.request("http://localhost").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()).isTable((table) -> table.withHeader("Parameter", "Description", "Foo") + .row("a", "one", "alpha") + .row("b", "two", "bravo")); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "form-parameters", template = "form-parameters-with-optional-column") + void formParametersWithOptionalColumn(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional(), + parameterWithName("b").description("two"))) + .document(operationBuilder.request("http://localhost").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Optional", "Description") + .row("a", "true", "one") + .row("b", "false", "two")); + } + + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + RequestDocumentation.formParameters(parameterWithName("a").description("one")) + .and(parameterWithName("b").description("two")) + .document(operationBuilder.request("http://localhost").content("a=alpha&b=bravo").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + } + + @RenderedSnippetTest + void additionalDescriptorsWithRelaxedFormParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + RequestDocumentation.relaxedFormParameters(parameterWithName("a").description("one")) + .and(parameterWithName("b").description("two")) + .document(operationBuilder.request("http://localhost").content("a=alpha&b=bravo&c=undocumented").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + } + + @RenderedSnippetTest + void formParametersWithEscapedContent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + RequestDocumentation.formParameters(parameterWithName("Foo|Bar").description("one|two")) + .document(operationBuilder.request("http://localhost").content("Foo%7CBar=baz").build()); + assertThat(snippets.formParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`Foo|Bar`", "one|two")); + } + + @SnippetTest + void undocumentedParameter(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new FormParametersSnippet(Collections.emptyList()) + .document(operationBuilder.request("http://localhost").content("a=alpha").build())) + .withMessage("Form parameters with the following names were not documented: [a]"); + } + + @SnippetTest + void missingParameter(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost").build())) + .withMessage("Form parameters with the following names were not found in the request: [a]"); + } + + @SnippetTest + void undocumentedAndMissingParameters(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new FormParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost").content("b=bravo").build())) + .withMessage("Form parameters with the following names were not documented: [b]. Form parameters" + + " with the following names were not found in the request: [a]"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java deleted file mode 100644 index 135ea8f0c..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.request; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.springframework.restdocs.generate.RestDocumentationGenerator; -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; - -/** - * Tests for failures when rendering {@link PathParametersSnippet} due to missing or - * undocumented path parameters. - * - * @author Andy Wilkinson - */ -public class PathParametersSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void undocumentedPathParameter() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Path parameters with the following names were" - + " not documented: [a]")); - new PathParametersSnippet(Collections.emptyList()) - .document(this.operationBuilder - .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/") - .build()); - } - - @Test - public void missingPathParameter() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Path parameters with the following names were" - + " not found in the request: [a]")); - new PathParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/").build()); - } - - @Test - public void undocumentedAndMissingPathParameters() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Path parameters with the following names were" - + " not documented: [b]. Path parameters with the following" - + " names were not found in the request: [a]")); - new PathParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{b}").build()); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index ef594c998..d178bde51 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,20 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.generate.RestDocumentationGenerator; -import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -41,193 +41,193 @@ * * @author Andy Wilkinson */ -public class PathParametersSnippetTests extends AbstractSnippetTests { +class PathParametersSnippetTests { - public PathParametersSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); + @RenderedSnippetTest + void pathParameters(OperationBuilder operationBuilder, AssertableSnippets snippets, TemplateFormat templateFormat) + throws IOException { + new PathParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") + .build()); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat), "Parameter", "Description") + .row("`a`", "one") + .row("`b`", "two")); } - @Test - public void pathParameters() throws IOException { - this.snippet.expectPathParameters().withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("`a`", "one").row("`b`", "two")); - new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), - parameterWithName("b").description("two"))) - .document(this.operationBuilder - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}") - .build()); - } - - @Test - public void ignoredPathParameter() throws IOException { - this.snippet.expectPathParameters().withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", - "two")); - new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), - parameterWithName("b").description("two"))) - .document(this.operationBuilder - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}") - .build()); - } - - @Test - public void allUndocumentedPathParametersCanBeIgnored() throws IOException { - this.snippet.expectPathParameters().withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", - "two")); + @RenderedSnippetTest + void ignoredPathParameter(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { new PathParametersSnippet( - Arrays.asList(parameterWithName("b").description("two")), true) - .document(this.operationBuilder.attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}").build()); + Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") + .build()); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat), "Parameter", "Description") + .row("`b`", "two")); } - @Test - public void missingOptionalPathParameter() throws IOException { - this.snippet.expectPathParameters().withContents(tableWithTitleAndHeader( - this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", - "Parameter", "Description").row("`a`", "one").row("`b`", "two")); + @RenderedSnippetTest + void allUndocumentedPathParametersCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { + new PathParametersSnippet(Arrays.asList(parameterWithName("b").description("two")), true).document( + operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}").build()); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat), "Parameter", "Description") + .row("`b`", "two")); + } + + @RenderedSnippetTest + void missingOptionalPathParameter(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two").optional())) - .document(this.operationBuilder.attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}").build()); + .document( + operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}").build()); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat, "/{a}"), "Parameter", "Description") + .row("`a`", "one") + .row("`b`", "two")); } - @Test - public void presentOptionalPathParameter() throws IOException { - this.snippet.expectPathParameters().withContents(tableWithTitleAndHeader( - this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", - "Parameter", "Description").row("`a`", "one")); - new PathParametersSnippet( - Arrays.asList(parameterWithName("a").description("one").optional())) - .document(this.operationBuilder.attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}").build()); + @RenderedSnippetTest + void presentOptionalPathParameter(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { + new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional())).document( + operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}").build()); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat, "/{a}"), "Parameter", "Description") + .row("`a`", "one")); } - @Test - public void pathParametersWithQueryString() throws IOException { - this.snippet.expectPathParameters().withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("`a`", "one").row("`b`", "two")); - new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), - parameterWithName("b").description("two"))) - .document(this.operationBuilder - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}?foo=bar") - .build()); - } - - @Test - public void pathParametersWithQueryStringWithParameters() throws IOException { - this.snippet.expectPathParameters().withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("`a`", "one").row("`b`", "two")); - new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), - parameterWithName("b").description("two"))) - .document(this.operationBuilder - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}?foo={c}") - .build()); + @RenderedSnippetTest + void pathParametersWithQueryString(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { + new PathParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) + .document(operationBuilder + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}?foo=bar") + .build()); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat), "Parameter", "Description") + .row("`a`", "one") + .row("`b`", "two")); } - @Test - public void pathParametersWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("path-parameters")) - .willReturn(snippetResource("path-parameters-with-title")); - this.snippet.expectPathParameters().withContents(containsString("The title")); + @RenderedSnippetTest + void pathParametersWithQueryStringWithParameters(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { + new PathParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) + .document(operationBuilder + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}?foo={c}") + .build()); + assertThat(snippets.pathParameters()) + .isTable((table) -> table.withTitleAndHeader(getTitle(templateFormat), "Parameter", "Description") + .row("`a`", "one") + .row("`b`", "two")); + } + @RenderedSnippetTest + @SnippetTemplate(snippet = "path-parameters", template = "path-parameters-with-title") + void pathParametersWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { new PathParametersSnippet( - Arrays.asList( - parameterWithName("a").description("one") - .attributes(key("foo").value("alpha")), - parameterWithName("b").description("two") - .attributes(key("foo").value("bravo"))), + Arrays.asList(parameterWithName("a").description("one").attributes(key("foo").value("alpha")), + parameterWithName("b").description("two").attributes(key("foo").value("bravo"))), attributes(key("title").value("The title"))) - .document(this.operationBuilder - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .build()); - - } - - @Test - public void pathParametersWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("path-parameters")) - .willReturn(snippetResource("path-parameters-with-extra-column")); - this.snippet.expectPathParameters() - .withContents(tableWithHeader("Parameter", "Description", "Foo") - .row("a", "one", "alpha").row("b", "two", "bravo")); - - new PathParametersSnippet(Arrays.asList( - parameterWithName("a").description("one") - .attributes(key("foo").value("alpha")), - parameterWithName("b").description("two") - .attributes(key("foo").value("bravo")))) - .document(this.operationBuilder - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .build()); - } - - @Test - public void additionalDescriptors() throws IOException { - this.snippet.expectPathParameters().withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("`a`", "one").row("`b`", "two")); + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") + .build()); + assertThat(snippets.pathParameters()).contains("The title"); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "path-parameters", template = "path-parameters-with-extra-column") + void pathParametersWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { + new PathParametersSnippet( + Arrays.asList(parameterWithName("a").description("one").attributes(key("foo").value("alpha")), + parameterWithName("b").description("two").attributes(key("foo").value("bravo")))) + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") + .build()); + assertThat(snippets.pathParameters()).isTable((table) -> table.withHeader("Parameter", "Description", "Foo") + .row("a", "one", "alpha") + .row("b", "two", "bravo")); + } + + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { RequestDocumentation.pathParameters(parameterWithName("a").description("one")) - .and(parameterWithName("b").description("two")) - .document(this.operationBuilder - .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}") - .build()); - } - - @Test - public void pathParametersWithEscapedContent() throws IOException { - this.snippet.expectPathParameters() - .withContents(tableWithTitleAndHeader(getTitle("{Foo|Bar}"), "Parameter", - "Description").row(escapeIfNecessary("`Foo|Bar`"), - escapeIfNecessary("one|two"))); - - RequestDocumentation - .pathParameters(parameterWithName("Foo|Bar").description("one|two")) - .document(this.operationBuilder - .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "{Foo|Bar}") - .build()); - } - - private String escapeIfNecessary(String input) { - if (this.templateFormat.equals(TemplateFormats.markdown())) { - return input; - } - return input.replace("|", "\\|"); + .and(parameterWithName("b").description("two")) + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") + .build()); + assertThat(snippets.pathParameters()).isTable( + (table) -> table.withTitleAndHeader(getTitle(templateFormat, "/{a}/{b}"), "Parameter", "Description") + .row("`a`", "one") + .row("`b`", "two")); + } + + @RenderedSnippetTest + void additionalDescriptorsWithRelaxedRequestParameters(OperationBuilder operationBuilder, + AssertableSnippets snippets, TemplateFormat templateFormat) throws IOException { + RequestDocumentation.relaxedPathParameters(parameterWithName("a").description("one")) + .and(parameterWithName("b").description("two")) + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}/{c}") + .build()); + assertThat(snippets.pathParameters()).isTable((table) -> table + .withTitleAndHeader(getTitle(templateFormat, "/{a}/{b}/{c}"), "Parameter", "Description") + .row("`a`", "one") + .row("`b`", "two")); + } + + @RenderedSnippetTest + void pathParametersWithEscapedContent(OperationBuilder operationBuilder, AssertableSnippets snippets, + TemplateFormat templateFormat) throws IOException { + RequestDocumentation.pathParameters(parameterWithName("Foo|Bar").description("one|two")) + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "{Foo|Bar}") + .build()); + assertThat(snippets.pathParameters()).isTable( + (table) -> table.withTitleAndHeader(getTitle(templateFormat, "{Foo|Bar}"), "Parameter", "Description") + .row("`Foo|Bar`", "one|two")); + } + + @SnippetTest + void undocumentedPathParameter(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new PathParametersSnippet(Collections.emptyList()) + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/") + .build())) + .withMessage("Path parameters with the following names were not documented: [a]"); + } + + @SnippetTest + void missingPathParameter(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/") + .build())) + .withMessage("Path parameters with the following names were not found in the request: [a]"); + } + + @SnippetTest + void undocumentedAndMissingPathParameters(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{b}") + .build())) + .withMessage("Path parameters with the following names were not documented: [b]. Path parameters with the" + + " following names were not found in the request: [a]"); } - private String getTitle() { - return getTitle("/{a}/{b}"); + private String getTitle(TemplateFormat templateFormat) { + return getTitle(templateFormat, "/{a}/{b}"); } - private String getTitle(String title) { - if (this.templateFormat.equals(TemplateFormats.asciidoctor())) { - return title; + private String getTitle(TemplateFormat templateFormat, String title) { + if (templateFormat.getId().equals(TemplateFormats.asciidoctor().getId())) { + return "+" + title + "+"; } return "`" + title + "`"; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java new file mode 100644 index 000000000..8084a2382 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/QueryParametersSnippetTests.java @@ -0,0 +1,188 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.request; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link QueryParametersSnippet}. + * + * @author Andy Wilkinson + */ +class QueryParametersSnippetTests { + + @RenderedSnippetTest + void queryParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new QueryParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) + .document(operationBuilder.request("http://localhost?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + } + + @RenderedSnippetTest + void queryParameterWithNoValue(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost?a").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one")); + } + + @RenderedSnippetTest + void ignoredQueryParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new QueryParametersSnippet( + Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) + .document(operationBuilder.request("http://localhost?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`b`", "two")); + } + + @RenderedSnippetTest + void allUndocumentedQueryParametersCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new QueryParametersSnippet(Arrays.asList(parameterWithName("b").description("two")), true) + .document(operationBuilder.request("http://localhost?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`b`", "two")); + } + + @RenderedSnippetTest + void missingOptionalQueryParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional(), + parameterWithName("b").description("two"))) + .document(operationBuilder.request("http://localhost?b=bravo").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + } + + @RenderedSnippetTest + void presentOptionalQueryParameter(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional())) + .document(operationBuilder.request("http://localhost?a=alpha").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one")); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "query-parameters", template = "query-parameters-with-title") + void queryParametersWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new QueryParametersSnippet( + Arrays.asList(parameterWithName("a").description("one").attributes(key("foo").value("alpha")), + parameterWithName("b").description("two").attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))) + .document(operationBuilder.request("http://localhost?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()).contains("The title"); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "query-parameters", template = "query-parameters-with-extra-column") + void queryParametersWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new QueryParametersSnippet( + Arrays.asList(parameterWithName("a").description("one").attributes(key("foo").value("alpha")), + parameterWithName("b").description("two").attributes(key("foo").value("bravo")))) + .document(operationBuilder.request("http://localhost?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()).isTable((table) -> table.withHeader("Parameter", "Description", "Foo") + .row("a", "one", "alpha") + .row("b", "two", "bravo")); + } + + @RenderedSnippetTest + @SnippetTemplate(snippet = "query-parameters", template = "query-parameters-with-optional-column") + void queryParametersWithOptionalColumn(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one").optional(), + parameterWithName("b").description("two"))) + .document(operationBuilder.request("http://localhost?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Optional", "Description") + .row("a", "true", "one") + .row("b", "false", "two")); + } + + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + RequestDocumentation.queryParameters(parameterWithName("a").description("one")) + .and(parameterWithName("b").description("two")) + .document(operationBuilder.request("http://localhost?a=alpha&b=bravo").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + } + + @RenderedSnippetTest + void additionalDescriptorsWithRelaxedQueryParameters(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + RequestDocumentation.relaxedQueryParameters(parameterWithName("a").description("one")) + .and(parameterWithName("b").description("two")) + .document(operationBuilder.request("http://localhost?a=alpha&b=bravo&c=undocumented").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`a`", "one").row("`b`", "two")); + } + + @RenderedSnippetTest + void queryParametersWithEscapedContent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + RequestDocumentation.queryParameters(parameterWithName("Foo|Bar").description("one|two")) + .document(operationBuilder.request("http://localhost?Foo%7CBar=baz").build()); + assertThat(snippets.queryParameters()) + .isTable((table) -> table.withHeader("Parameter", "Description").row("`Foo|Bar`", "one|two")); + } + + @SnippetTest + void undocumentedParameter(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new QueryParametersSnippet(Collections.emptyList()) + .document(operationBuilder.request("http://localhost?a=alpha").build())) + .withMessage("Query parameters with the following names were not documented: [a]"); + } + + @SnippetTest + void missingParameter(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost").build())) + .withMessage("Query parameters with the following names were not found in the request: [a]"); + } + + @SnippetTest + void undocumentedAndMissingParameters(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new QueryParametersSnippet(Arrays.asList(parameterWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost?b=bravo").build())) + .withMessage("Query parameters with the following names were not documented: [b]. Query parameters" + + " with the following names were not found in the request: [a]"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java deleted file mode 100644 index 94ce7be73..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.request; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; - -/** - * Tests for failures when rendering {@link RequestParametersSnippet} due to missing or - * undocumented request parameters. - * - * @author Andy Wilkinson - */ -public class RequestParametersSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void undocumentedParameter() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Request parameters with the following names were" - + " not documented: [a]")); - new RequestParametersSnippet(Collections.emptyList()) - .document(this.operationBuilder.request("http://localhost") - .param("a", "alpha").build()); - } - - @Test - public void missingParameter() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Request parameters with the following names were" - + " not found in the request: [a]")); - new RequestParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))).document( - this.operationBuilder.request("http://localhost").build()); - } - - @Test - public void undocumentedAndMissingParameters() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Request parameters with the following names were" - + " not documented: [b]. Request parameters with the following" - + " names were not found in the request: [a]")); - new RequestParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost") - .param("b", "bravo").build()); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java deleted file mode 100644 index 4c13e4ecc..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.request; - -import java.io.IOException; -import java.util.Arrays; - -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; - -/** - * Tests for {@link RequestParametersSnippet}. - * - * @author Andy Wilkinson - */ -public class RequestParametersSnippetTests extends AbstractSnippetTests { - - public RequestParametersSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } - - @Test - public void requestParameters() throws IOException { - this.snippet.expectRequestParameters() - .withContents(tableWithHeader("Parameter", "Description") - .row("`a`", "one").row("`b`", "two")); - new RequestParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"), - parameterWithName("b").description("two"))).document( - this.operationBuilder.request("http://localhost") - .param("a", "bravo").param("b", "bravo").build()); - } - - @Test - public void requestParameterWithNoValue() throws IOException { - this.snippet.expectRequestParameters().withContents( - tableWithHeader("Parameter", "Description").row("`a`", "one")); - new RequestParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost") - .param("a").build()); - } - - @Test - public void ignoredRequestParameter() throws IOException { - this.snippet.expectRequestParameters().withContents( - tableWithHeader("Parameter", "Description").row("`b`", "two")); - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), - parameterWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost") - .param("a", "bravo").param("b", "bravo").build()); - } - - @Test - public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { - this.snippet.expectRequestParameters().withContents( - tableWithHeader("Parameter", "Description").row("`b`", "two")); - new RequestParametersSnippet( - Arrays.asList(parameterWithName("b").description("two")), true) - .document(this.operationBuilder.request("http://localhost") - .param("a", "bravo").param("b", "bravo").build()); - } - - @Test - public void missingOptionalRequestParameter() throws IOException { - this.snippet.expectRequestParameters() - .withContents(tableWithHeader("Parameter", "Description") - .row("`a`", "one").row("`b`", "two")); - new RequestParametersSnippet( - Arrays.asList(parameterWithName("a").description("one").optional(), - parameterWithName("b").description("two"))).document( - this.operationBuilder.request("http://localhost") - .param("b", "bravo").build()); - } - - @Test - public void presentOptionalRequestParameter() throws IOException { - this.snippet.expectRequestParameters().withContents( - tableWithHeader("Parameter", "Description").row("`a`", "one")); - new RequestParametersSnippet( - Arrays.asList(parameterWithName("a").description("one").optional())) - .document(this.operationBuilder.request("http://localhost") - .param("a", "one").build()); - } - - @Test - public void requestParametersWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parameters")) - .willReturn(snippetResource("request-parameters-with-title")); - this.snippet.expectRequestParameters().withContents(containsString("The title")); - - new RequestParametersSnippet( - Arrays.asList( - parameterWithName("a").description("one") - .attributes(key("foo").value("alpha")), - parameterWithName("b").description("two") - .attributes(key("foo").value("bravo"))), - attributes( - key("title").value("The title"))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .param("a", "alpha").param("b", "bravo") - .build()); - } - - @Test - public void requestParametersWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parameters")) - .willReturn(snippetResource("request-parameters-with-extra-column")); - this.snippet.expectRequestParameters() - .withContents(tableWithHeader("Parameter", "Description", "Foo") - .row("a", "one", "alpha").row("b", "two", "bravo")); - - new RequestParametersSnippet(Arrays.asList( - parameterWithName("a").description("one") - .attributes(key("foo").value("alpha")), - parameterWithName("b").description("two") - .attributes(key("foo").value("bravo")))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .param("a", "alpha").param("b", "bravo") - .build()); - } - - @Test - public void requestParametersWithOptionalColumn() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parameters")) - .willReturn(snippetResource("request-parameters-with-optional-column")); - this.snippet.expectRequestParameters() - .withContents(tableWithHeader("Parameter", "Optional", "Description") - .row("a", "true", "one").row("b", "false", "two")); - - new RequestParametersSnippet( - Arrays.asList(parameterWithName("a").description("one").optional(), - parameterWithName("b").description("two"))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .param("a", "alpha").param("b", "bravo") - .build()); - } - - @Test - public void additionalDescriptors() throws IOException { - this.snippet.expectRequestParameters() - .withContents(tableWithHeader("Parameter", "Description") - .row("`a`", "one").row("`b`", "two")); - RequestDocumentation.requestParameters(parameterWithName("a").description("one")) - .and(parameterWithName("b").description("two")) - .document(this.operationBuilder.request("http://localhost") - .param("a", "bravo").param("b", "bravo").build()); - } - - @Test - public void requestParametersWithEscapedContent() throws IOException { - this.snippet.expectRequestParameters() - .withContents(tableWithHeader("Parameter", "Description").row( - escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); - - RequestDocumentation - .requestParameters(parameterWithName("Foo|Bar").description("one|two")) - .document(this.operationBuilder.request("http://localhost") - .param("Foo|Bar", "baz").build()); - } - - private String escapeIfNecessary(String input) { - if (this.templateFormat.equals(TemplateFormats.markdown())) { - return input; - } - return input.replace("|", "\\|"); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java deleted file mode 100644 index 1092fe5cc..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.request; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; -import org.springframework.restdocs.test.OperationBuilder; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.springframework.restdocs.request.RequestDocumentation.partWithName; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; - -/** - * Tests for failures when rendering {@link RequestPartsSnippet} due to missing or - * undocumented request parts. - * - * @author Andy Wilkinson - */ -public class RequestPartsSnippetFailureTests { - - @Rule - public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void undocumentedPart() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo( - "Request parts with the following names were" + " not documented: [a]")); - new RequestPartsSnippet(Collections.emptyList()) - .document(this.operationBuilder.request("http://localhost") - .part("a", "alpha".getBytes()).build()); - } - - @Test - public void missingPart() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Request parts with the following names were" - + " not found in the request: [a]")); - new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost").build()); - } - - @Test - public void undocumentedAndMissingParts() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Request parts with the following names were" - + " not documented: [b]. Request parts with the following" - + " names were not found in the request: [a]")); - new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) - .document(this.operationBuilder.request("http://localhost") - .part("b", "bravo".getBytes()).build()); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java index 952a5b026..9dcc12f9f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,17 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; -import org.junit.Test; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTemplate; +import org.springframework.restdocs.testfixtures.jupiter.SnippetTest; -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.templates.TemplateResourceResolver; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -40,166 +38,157 @@ * * @author Andy Wilkinson */ -public class RequestPartsSnippetTests extends AbstractSnippetTests { - - public RequestPartsSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } +class RequestPartsSnippetTests { - @Test - public void requestParts() throws IOException { - this.snippet.expectRequestParts() - .withContents(tableWithHeader("Part", "Description").row("`a`", "one") - .row("`b`", "two")); - new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"), - partWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost") - .part("a", "bravo".getBytes()).and() - .part("b", "bravo".getBytes()).build()); + @RenderedSnippetTest + void requestParts(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestPartsSnippet( + Arrays.asList(partWithName("a").description("one"), partWithName("b").description("two"))) + .document(operationBuilder.request("http://localhost") + .part("a", "bravo".getBytes()) + .and() + .part("b", "bravo".getBytes()) + .build()); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void ignoredRequestPart() throws IOException { - this.snippet.expectRequestParts() - .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); - new RequestPartsSnippet(Arrays.asList(partWithName("a").ignored(), - partWithName("b").description("two"))) - .document(this.operationBuilder.request("http://localhost") - .part("a", "bravo".getBytes()).and() - .part("b", "bravo".getBytes()).build()); + @RenderedSnippetTest + void ignoredRequestPart(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestPartsSnippet(Arrays.asList(partWithName("a").ignored(), partWithName("b").description("two"))) + .document(operationBuilder.request("http://localhost") + .part("a", "bravo".getBytes()) + .and() + .part("b", "bravo".getBytes()) + .build()); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`b`", "two")); } - @Test - public void allUndocumentedRequestPartsCanBeIgnored() throws IOException { - this.snippet.expectRequestParts() - .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); + @RenderedSnippetTest + void allUndocumentedRequestPartsCanBeIgnored(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestPartsSnippet(Arrays.asList(partWithName("b").description("two")), true) - .document(this.operationBuilder.request("http://localhost") - .part("a", "bravo".getBytes()).and().part("b", "bravo".getBytes()) - .build()); + .document(operationBuilder.request("http://localhost") + .part("a", "bravo".getBytes()) + .and() + .part("b", "bravo".getBytes()) + .build()); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`b`", "two")); } - @Test - public void missingOptionalRequestPart() throws IOException { - this.snippet.expectRequestParts() - .withContents(tableWithHeader("Part", "Description").row("`a`", "one") - .row("`b`", "two")); + @RenderedSnippetTest + void missingOptionalRequestPart(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { new RequestPartsSnippet( - Arrays.asList(partWithName("a").description("one").optional(), - partWithName("b").description("two"))).document( - this.operationBuilder.request("http://localhost") - .part("b", "bravo".getBytes()).build()); + Arrays.asList(partWithName("a").description("one").optional(), partWithName("b").description("two"))) + .document(operationBuilder.request("http://localhost").part("b", "bravo".getBytes()).build()); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void presentOptionalRequestPart() throws IOException { - this.snippet.expectRequestParts() - .withContents(tableWithHeader("Part", "Description").row("`a`", "one")); - new RequestPartsSnippet( - Arrays.asList(partWithName("a").description("one").optional())) - .document(this.operationBuilder.request("http://localhost") - .part("a", "one".getBytes()).build()); + @RenderedSnippetTest + void presentOptionalRequestPart(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { + new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one").optional())) + .document(operationBuilder.request("http://localhost").part("a", "one".getBytes()).build()); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`a`", "one")); } - @Test - public void requestPartsWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parts")) - .willReturn(snippetResource("request-parts-with-title")); - this.snippet.expectRequestParts().withContents(containsString("The title")); - + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-parts", template = "request-parts-with-title") + void requestPartsWithCustomAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestPartsSnippet( - Arrays.asList( - partWithName("a").description("one") - .attributes(key("foo").value("alpha")), - partWithName("b").description("two") - .attributes(key("foo").value("bravo"))), - attributes( - key("title").value("The title"))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .part("a", "alpha".getBytes()).and() - .part("b", "bravo".getBytes()).build()); + Arrays.asList(partWithName("a").description("one").attributes(key("foo").value("alpha")), + partWithName("b").description("two").attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))) + .document(operationBuilder.request("http://localhost") + .part("a", "alpha".getBytes()) + .and() + .part("b", "bravo".getBytes()) + .build()); + assertThat(snippets.requestParts()).contains("The title"); } - @Test - public void requestPartsWithCustomDescriptorAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parts")) - .willReturn(snippetResource("request-parts-with-extra-column")); - this.snippet.expectRequestParts() - .withContents(tableWithHeader("Part", "Description", "Foo") - .row("a", "one", "alpha").row("b", "two", "bravo")); - - new RequestPartsSnippet(Arrays.asList( - partWithName("a").description("one") - .attributes(key("foo").value("alpha")), - partWithName("b").description("two") - .attributes(key("foo").value("bravo")))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .part("a", "alpha".getBytes()).and() - .part("b", "bravo".getBytes()).build()); + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-parts", template = "request-parts-with-extra-column") + void requestPartsWithCustomDescriptorAttributes(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { + new RequestPartsSnippet( + Arrays.asList(partWithName("a").description("one").attributes(key("foo").value("alpha")), + partWithName("b").description("two").attributes(key("foo").value("bravo")))) + .document(operationBuilder.request("http://localhost") + .part("a", "alpha".getBytes()) + .and() + .part("b", "bravo".getBytes()) + .build()); + assertThat(snippets.requestParts()).isTable((table) -> table.withHeader("Part", "Description", "Foo") + .row("a", "one", "alpha") + .row("b", "two", "bravo")); } - @Test - public void requestPartsWithOptionalColumn() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parts")) - .willReturn(snippetResource("request-parts-with-optional-column")); - this.snippet.expectRequestParts() - .withContents(tableWithHeader("Part", "Optional", "Description") - .row("a", "true", "one").row("b", "false", "two")); - + @RenderedSnippetTest + @SnippetTemplate(snippet = "request-parts", template = "request-parts-with-optional-column") + void requestPartsWithOptionalColumn(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { new RequestPartsSnippet( - Arrays.asList(partWithName("a").description("one").optional(), - partWithName("b").description("two"))) - .document( - this.operationBuilder - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .part("a", "alpha".getBytes()).and() - .part("b", "bravo".getBytes()).build()); + Arrays.asList(partWithName("a").description("one").optional(), partWithName("b").description("two"))) + .document(operationBuilder.request("http://localhost") + .part("a", "alpha".getBytes()) + .and() + .part("b", "bravo".getBytes()) + .build()); + assertThat(snippets.requestParts()).isTable((table) -> table.withHeader("Part", "Optional", "Description") + .row("a", "true", "one") + .row("b", "false", "two")); } - @Test - public void additionalDescriptors() throws IOException { - this.snippet.expectRequestParts() - .withContents(tableWithHeader("Part", "Description").row("`a`", "one") - .row("`b`", "two")); + @RenderedSnippetTest + void additionalDescriptors(OperationBuilder operationBuilder, AssertableSnippets snippets) throws IOException { RequestDocumentation.requestParts(partWithName("a").description("one")) - .and(partWithName("b").description("two")) - .document(this.operationBuilder.request("http://localhost") - .part("a", "bravo".getBytes()).and().part("b", "bravo".getBytes()) - .build()); + .and(partWithName("b").description("two")) + .document(operationBuilder.request("http://localhost") + .part("a", "bravo".getBytes()) + .and() + .part("b", "bravo".getBytes()) + .build()); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`a`", "one").row("`b`", "two")); } - @Test - public void requestPartsWithEscapedContent() throws IOException { - this.snippet.expectRequestParts().withContents( - tableWithHeader("Part", "Description").row(escapeIfNecessary("`Foo|Bar`"), - escapeIfNecessary("one|two"))); - + @RenderedSnippetTest + void requestPartsWithEscapedContent(OperationBuilder operationBuilder, AssertableSnippets snippets) + throws IOException { RequestDocumentation.requestParts(partWithName("Foo|Bar").description("one|two")) - .document(this.operationBuilder.request("http://localhost") - .part("Foo|Bar", "baz".getBytes()).build()); + .document(operationBuilder.request("http://localhost").part("Foo|Bar", "baz".getBytes()).build()); + assertThat(snippets.requestParts()) + .isTable((table) -> table.withHeader("Part", "Description").row("`Foo|Bar`", "one|two")); + } + + @SnippetTest + void undocumentedPart(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestPartsSnippet(Collections.emptyList()) + .document(operationBuilder.request("http://localhost").part("a", "alpha".getBytes()).build())) + .withMessage("Request parts with the following names were not documented: [a]"); + } + + @SnippetTest + void missingPart(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost").build())) + .withMessage("Request parts with the following names were not found in the request: [a]"); } - private String escapeIfNecessary(String input) { - if (this.templateFormat.equals(TemplateFormats.markdown())) { - return input; - } - return input.replace("|", "\\|"); + @SnippetTest + void undocumentedAndMissingParts(OperationBuilder operationBuilder) { + assertThatExceptionOfType(SnippetException.class) + .isThrownBy(() -> new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) + .document(operationBuilder.request("http://localhost").part("b", "bravo".getBytes()).build())) + .withMessage("Request parts with the following names were not documented: [b]. Request parts with the" + + " following names were not found in the request: [a]"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index 0c2f02820..22b285c30 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,13 @@ package org.springframework.restdocs.snippet; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link RestDocumentationContextPlaceholderResolver}. @@ -31,47 +30,83 @@ * @author Andy Wilkinson * */ -public class RestDocumentationContextPlaceholderResolverTests { +class RestDocumentationContextPlaceholderResolverTests { @Test - public void kebabCaseMethodName() throws Exception { - assertThat(createResolver("dashSeparatedMethodName").resolvePlaceholder( - "method-name"), equalTo("dash-separated-method-name")); + void kebabCaseMethodName() { + assertThat(createResolver("dashSeparatedMethodName").resolvePlaceholder("method-name")) + .isEqualTo("dash-separated-method-name"); } @Test - public void snakeCaseMethodName() throws Exception { - assertThat(createResolver("underscoreSeparatedMethodName").resolvePlaceholder( - "method_name"), equalTo("underscore_separated_method_name")); + void kebabCaseMethodNameWithUpperCaseOpeningSection() { + assertThat(createResolver("URIDashSeparatedMethodName").resolvePlaceholder("method-name")) + .isEqualTo("uri-dash-separated-method-name"); } @Test - public void camelCaseMethodName() throws Exception { - assertThat(createResolver("camelCaseMethodName").resolvePlaceholder("methodName"), - equalTo("camelCaseMethodName")); + void kebabCaseMethodNameWithUpperCaseMidSection() { + assertThat(createResolver("dashSeparatedMethodNameWithURIInIt").resolvePlaceholder("method-name")) + .isEqualTo("dash-separated-method-name-with-uri-in-it"); } @Test - public void kebabCaseClassName() throws Exception { - assertThat(createResolver().resolvePlaceholder("class-name"), - equalTo("rest-documentation-context-placeholder-resolver-tests")); + void kebabCaseMethodNameWithUpperCaseEndSection() { + assertThat(createResolver("dashSeparatedMethodNameWithURI").resolvePlaceholder("method-name")) + .isEqualTo("dash-separated-method-name-with-uri"); } @Test - public void snakeCaseClassName() throws Exception { - assertThat(createResolver().resolvePlaceholder("class_name"), - equalTo("rest_documentation_context_placeholder_resolver_tests")); + void snakeCaseMethodName() { + assertThat(createResolver("underscoreSeparatedMethodName").resolvePlaceholder("method_name")) + .isEqualTo("underscore_separated_method_name"); } @Test - public void camelCaseClassName() throws Exception { - assertThat(createResolver().resolvePlaceholder("ClassName"), - equalTo("RestDocumentationContextPlaceholderResolverTests")); + void snakeCaseMethodNameWithUpperCaseOpeningSection() { + assertThat(createResolver("URIUnderscoreSeparatedMethodName").resolvePlaceholder("method_name")) + .isEqualTo("uri_underscore_separated_method_name"); } @Test - public void stepCount() throws Exception { - assertThat(createResolver("stepCount").resolvePlaceholder("step"), equalTo("1")); + void snakeCaseMethodNameWithUpperCaseMidSection() { + assertThat(createResolver("underscoreSeparatedMethodNameWithURIInIt").resolvePlaceholder("method_name")) + .isEqualTo("underscore_separated_method_name_with_uri_in_it"); + } + + @Test + void snakeCaseMethodNameWithUpperCaseEndSection() { + assertThat(createResolver("underscoreSeparatedMethodNameWithURI").resolvePlaceholder("method_name")) + .isEqualTo("underscore_separated_method_name_with_uri"); + } + + @Test + void camelCaseMethodName() { + assertThat(createResolver("camelCaseMethodName").resolvePlaceholder("methodName")) + .isEqualTo("camelCaseMethodName"); + } + + @Test + void kebabCaseClassName() { + assertThat(createResolver().resolvePlaceholder("class-name")) + .isEqualTo("rest-documentation-context-placeholder-resolver-tests"); + } + + @Test + void snakeCaseClassName() { + assertThat(createResolver().resolvePlaceholder("class_name")) + .isEqualTo("rest_documentation_context_placeholder_resolver_tests"); + } + + @Test + void camelCaseClassName() { + assertThat(createResolver().resolvePlaceholder("ClassName")) + .isEqualTo("RestDocumentationContextPlaceholderResolverTests"); + } + + @Test + void stepCount() { + assertThat(createResolver("stepCount").resolvePlaceholder("step")).isEqualTo("1"); } private PlaceholderResolver createResolver() { @@ -83,8 +118,7 @@ private PlaceholderResolver createResolver(String methodName) { } private RestDocumentationContext createContext(String methodName) { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( - "build"); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation("build"); manualRestDocumentation.beforeTest(getClass(), methodName); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 86944f237..0df5118d1 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,64 +17,96 @@ package org.springframework.restdocs.snippet; import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Writer; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for {@link StandardWriterResolver}. * * @author Andy Wilkinson */ -public class StandardWriterResolverTests { +class StandardWriterResolverTests { - private final PlaceholderResolverFactory placeholderResolverFactory = mock( - PlaceholderResolverFactory.class); + @TempDir + File temp; - private final StandardWriterResolver resolver = new StandardWriterResolver( - this.placeholderResolverFactory, "UTF-8", asciidoctor()); + private final PlaceholderResolverFactory placeholderResolverFactory = mock(PlaceholderResolverFactory.class); + + private final StandardWriterResolver resolver = new StandardWriterResolver(this.placeholderResolverFactory, "UTF-8", + TemplateFormats.asciidoctor()); @Test - public void absoluteInput() { + void absoluteInput() { String absolutePath = new File("foo").getAbsolutePath(); - assertThat( - this.resolver.resolveFile(absolutePath, "bar.txt", - createContext(absolutePath)), - is(new File(absolutePath, "bar.txt"))); + assertThat(this.resolver.resolveFile(absolutePath, "bar.txt", createContext(absolutePath))) + .isEqualTo(new File(absolutePath, "bar.txt")); } @Test - public void configuredOutputAndRelativeInput() { + void configuredOutputAndRelativeInput() { File outputDir = new File("foo").getAbsoluteFile(); - assertThat( - this.resolver.resolveFile("bar", "baz.txt", - createContext(outputDir.getAbsolutePath())), - is(new File(outputDir, "bar/baz.txt"))); + assertThat(this.resolver.resolveFile("bar", "baz.txt", createContext(outputDir.getAbsolutePath()))) + .isEqualTo(new File(outputDir, "bar/baz.txt")); } @Test - public void configuredOutputAndAbsoluteInput() { + void configuredOutputAndAbsoluteInput() { File outputDir = new File("foo").getAbsoluteFile(); String absolutePath = new File("bar").getAbsolutePath(); - assertThat( - this.resolver.resolveFile(absolutePath, "baz.txt", - createContext(outputDir.getAbsolutePath())), - is(new File(absolutePath, "baz.txt"))); + assertThat(this.resolver.resolveFile(absolutePath, "baz.txt", createContext(outputDir.getAbsolutePath()))) + .isEqualTo(new File(absolutePath, "baz.txt")); + } + + @Test + void placeholdersAreResolvedInOperationName() throws IOException { + File outputDirectory = this.temp; + RestDocumentationContext context = createContext(outputDirectory.getAbsolutePath()); + PlaceholderResolver resolver = mock(PlaceholderResolver.class); + given(resolver.resolvePlaceholder("a")).willReturn("alpha"); + given(this.placeholderResolverFactory.create(context)).willReturn(resolver); + try (Writer writer = this.resolver.resolve("{a}", "bravo", context)) { + assertSnippetLocation(writer, new File(outputDirectory, "alpha/bravo.adoc")); + } + } + + @Test + void placeholdersAreResolvedInSnippetName() throws IOException { + File outputDirectory = this.temp; + RestDocumentationContext context = createContext(outputDirectory.getAbsolutePath()); + PlaceholderResolver resolver = mock(PlaceholderResolver.class); + given(resolver.resolvePlaceholder("b")).willReturn("bravo"); + given(this.placeholderResolverFactory.create(context)).willReturn(resolver); + try (Writer writer = this.resolver.resolve("alpha", "{b}", context)) { + assertSnippetLocation(writer, new File(outputDirectory, "alpha/bravo.adoc")); + } } private RestDocumentationContext createContext(String outputDir) { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( - outputDir); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation(outputDir); manualRestDocumentation.beforeTest(getClass(), null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; } + private void assertSnippetLocation(Writer writer, File expectedLocation) throws IOException { + writer.write("test"); + writer.flush(); + assertThat(expectedLocation).exists(); + assertThat(FileCopyUtils.copyToString(new FileReader(expectedLocation))).isEqualTo("test"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java index 1e570a961..724e68a5d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,59 +16,70 @@ package org.springframework.restdocs.snippet; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.testfixtures.jupiter.AssertableSnippets; +import org.springframework.restdocs.testfixtures.jupiter.OperationBuilder; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.Matchers.hasEntry; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link TemplatedSnippet}. * * @author Andy Wilkinson */ -public class TemplatedSnippetTests { +class TemplatedSnippetTests { @Test - public void attributesAreCopied() { + void attributesAreCopied() { Map attributes = new HashMap<>(); attributes.put("a", "alpha"); TemplatedSnippet snippet = new TestTemplatedSnippet(attributes); attributes.put("b", "bravo"); - assertThat(snippet.getAttributes().size(), is(1)); - assertThat(snippet.getAttributes(), hasEntry("a", (Object) "alpha")); + assertThat(snippet.getAttributes()).hasSize(1); + assertThat(snippet.getAttributes()).containsEntry("a", "alpha"); } @Test - public void nullAttributesAreTolerated() { - assertThat(new TestTemplatedSnippet(null).getAttributes(), is(not(nullValue()))); - assertThat(new TestTemplatedSnippet(null).getAttributes().size(), is(0)); + void nullAttributesAreTolerated() { + assertThat(new TestTemplatedSnippet(null).getAttributes()).isNotNull(); + assertThat(new TestTemplatedSnippet(null).getAttributes()).isEmpty(); } @Test - public void snippetName() { - assertThat(new TestTemplatedSnippet(Collections.emptyMap()) - .getSnippetName(), is(equalTo("test"))); + void snippetName() { + assertThat(new TestTemplatedSnippet(Collections.emptyMap()).getSnippetName()).isEqualTo("test"); + } + + @RenderedSnippetTest + void multipleSnippetsCanBeProducedFromTheSameTemplate(OperationBuilder operationBuilder, AssertableSnippets snippet) + throws IOException { + new TestTemplatedSnippet("one", "multiple-snippets").document(operationBuilder.build()); + new TestTemplatedSnippet("two", "multiple-snippets").document(operationBuilder.build()); + assertThat(snippet.named("multiple-snippets-one")).exists(); + assertThat(snippet.named("multiple-snippets-two")).exists(); } private static class TestTemplatedSnippet extends TemplatedSnippet { + protected TestTemplatedSnippet(String snippetName, String templateName) { + super(templateName + "-" + snippetName, templateName, Collections.emptyMap()); + } + protected TestTemplatedSnippet(Map attributes) { super("test", attributes); } @Override protected Map createModel(Operation operation) { - return null; + return new HashMap<>(); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java index c364aec1d..4f5646337 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,135 +16,111 @@ package org.springframework.restdocs.templates; +import java.io.IOException; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.junit.jupiter.api.Test; import org.springframework.core.io.Resource; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link TemplateResourceResolver}. * * @author Andy Wilkinson */ -public class StandardTemplateResourceResolverTests { - - @Rule - public final ExpectedException thrown = ExpectedException.none(); +class StandardTemplateResourceResolverTests { private final TemplateResourceResolver resolver = new StandardTemplateResourceResolver( - asciidoctor()); + TemplateFormats.asciidoctor()); private final TestClassLoader classLoader = new TestClassLoader(); @Test - public void formatSpecificCustomSnippetHasHighestPrecedence() throws Exception { - this.classLoader.addResource( - "org/springframework/restdocs/templates/asciidoctor/test.snippet", + void formatSpecificCustomSnippetHasHighestPrecedence() throws IOException { + this.classLoader.addResource("org/springframework/restdocs/templates/asciidoctor/test.snippet", getClass().getResource("test-format-specific-custom.snippet")); - this.classLoader.addResource( - "org/springframework/restdocs/templates/test.snippet", + this.classLoader.addResource("org/springframework/restdocs/templates/test.snippet", getClass().getResource("test-custom.snippet")); - this.classLoader.addResource( - "org/springframework/restdocs/templates/asciidoctor/default-test.snippet", + this.classLoader.addResource("org/springframework/restdocs/templates/asciidoctor/default-test.snippet", getClass().getResource("test-default.snippet")); - Resource snippet = doWithThreadContextClassLoader(this.classLoader, - new Callable() { + Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { - @Override - public Resource call() { - return StandardTemplateResourceResolverTests.this.resolver - .resolveTemplateResource("test"); - } + @Override + public Resource call() { + return StandardTemplateResourceResolverTests.this.resolver.resolveTemplateResource("test"); + } - }); + }); - assertThat(snippet.getURL(), is( - equalTo(getClass().getResource("test-format-specific-custom.snippet")))); + assertThat(snippet.getURL()).isEqualTo(getClass().getResource("test-format-specific-custom.snippet")); } @Test - public void generalCustomSnippetIsUsedInAbsenceOfFormatSpecificCustomSnippet() - throws Exception { - this.classLoader.addResource( - "org/springframework/restdocs/templates/test.snippet", + void generalCustomSnippetIsUsedInAbsenceOfFormatSpecificCustomSnippet() throws IOException { + this.classLoader.addResource("org/springframework/restdocs/templates/test.snippet", getClass().getResource("test-custom.snippet")); - this.classLoader.addResource( - "org/springframework/restdocs/templates/asciidoctor/default-test.snippet", + this.classLoader.addResource("org/springframework/restdocs/templates/asciidoctor/default-test.snippet", getClass().getResource("test-default.snippet")); - Resource snippet = doWithThreadContextClassLoader(this.classLoader, - new Callable() { + Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { - @Override - public Resource call() { - return StandardTemplateResourceResolverTests.this.resolver - .resolveTemplateResource("test"); - } + @Override + public Resource call() { + return StandardTemplateResourceResolverTests.this.resolver.resolveTemplateResource("test"); + } - }); + }); - assertThat(snippet.getURL(), - is(equalTo(getClass().getResource("test-custom.snippet")))); + assertThat(snippet.getURL()).isEqualTo(getClass().getResource("test-custom.snippet")); } @Test - public void defaultSnippetIsUsedInAbsenceOfCustomSnippets() throws Exception { - this.classLoader.addResource( - "org/springframework/restdocs/templates/asciidoctor/default-test.snippet", + void defaultSnippetIsUsedInAbsenceOfCustomSnippets() throws Exception { + this.classLoader.addResource("org/springframework/restdocs/templates/asciidoctor/default-test.snippet", getClass().getResource("test-default.snippet")); - Resource snippet = doWithThreadContextClassLoader(this.classLoader, - new Callable() { - - @Override - public Resource call() { - return StandardTemplateResourceResolverTests.this.resolver - .resolveTemplateResource("test"); - } - - }); - - assertThat(snippet.getURL(), - is(equalTo(getClass().getResource("test-default.snippet")))); - } - - @Test - public void failsIfCustomAndDefaultSnippetsDoNotExist() throws Exception { - this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage(equalTo("Template named 'test' could not be resolved")); - doWithThreadContextClassLoader(this.classLoader, new Callable() { + Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { @Override public Resource call() { - return StandardTemplateResourceResolverTests.this.resolver - .resolveTemplateResource("test"); + return StandardTemplateResourceResolverTests.this.resolver.resolveTemplateResource("test"); } }); + + assertThat(snippet.getURL()).isEqualTo(getClass().getResource("test-default.snippet")); } - private T doWithThreadContextClassLoader(ClassLoader classLoader, - Callable action) throws Exception { + @Test + void failsIfCustomAndDefaultSnippetsDoNotExist() { + assertThatIllegalStateException() + .isThrownBy(() -> doWithThreadContextClassLoader(this.classLoader, + () -> StandardTemplateResourceResolverTests.this.resolver.resolveTemplateResource("test"))) + .withMessage("Template named 'test' could not be resolved"); + } + + private T doWithThreadContextClassLoader(ClassLoader classLoader, Callable action) { ClassLoader previous = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(classLoader); try { return action.call(); } + catch (Exception ex) { + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + throw new RuntimeException(ex); + } finally { Thread.currentThread().setContextClassLoader(previous); } } - private static class TestClassLoader extends ClassLoader { + private static final class TestClassLoader extends ClassLoader { private Map resources = new HashMap<>(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java index 2a6791554..635a80ae5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,11 @@ import java.io.IOException; import java.io.StringWriter; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.restdocs.mustache.Template.Fragment; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -34,24 +32,24 @@ * * @author Andy Wilkinson */ -public class AsciidoctorTableCellContentLambdaTests { +class AsciidoctorTableCellContentLambdaTests { @Test - public void verticalBarCharactersAreEscaped() throws IOException { + void verticalBarCharactersAreEscaped() throws IOException { Fragment fragment = mock(Fragment.class); given(fragment.execute()).willReturn("|foo|bar|baz|"); StringWriter writer = new StringWriter(); new AsciidoctorTableCellContentLambda().execute(fragment, writer); - assertThat(writer.toString(), is(equalTo("\\|foo\\|bar\\|baz\\|"))); + assertThat(writer.toString()).isEqualTo("\\|foo\\|bar\\|baz\\|"); } @Test - public void escapedVerticalBarCharactersAreNotEscapedAgain() throws IOException { + void escapedVerticalBarCharactersAreNotEscapedAgain() throws IOException { Fragment fragment = mock(Fragment.class); given(fragment.execute()).willReturn("\\|foo|bar\\|baz|"); StringWriter writer = new StringWriter(); new AsciidoctorTableCellContentLambda().execute(fragment, writer); - assertThat(writer.toString(), is(equalTo("\\|foo\\|bar\\|baz\\|"))); + assertThat(writer.toString()).isEqualTo("\\|foo\\|bar\\|baz\\|"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java deleted file mode 100644 index fa17e7521..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2014-2015 the original author or 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. - */ - -package org.springframework.restdocs.test; - -import java.io.File; -import java.io.IOException; - -import org.hamcrest.Matcher; -import org.junit.runners.model.Statement; - -import org.springframework.restdocs.snippet.TemplatedSnippet; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -/** - * The {@code ExpectedSnippet} rule is used to verify that a {@link TemplatedSnippet} has - * generated the expected snippet. - * - * @author Andy Wilkinson - * @author Andreas Evers - */ -public class ExpectedSnippet extends OperationTestRule { - - private final TemplateFormat templateFormat; - - private final SnippetMatcher snippet; - - private String expectedName; - - private String expectedType; - - private File outputDirectory; - - public ExpectedSnippet(TemplateFormat templateFormat) { - this.templateFormat = templateFormat; - this.snippet = SnippetMatchers.snippet(templateFormat); - } - - @Override - public Statement apply(final Statement base, File outputDirectory, - String operationName) { - this.outputDirectory = outputDirectory; - this.expectedName = operationName; - return new ExpectedSnippetStatement(base); - } - - private void verifySnippet() throws IOException { - if (this.outputDirectory != null && this.expectedName != null) { - File snippetDir = new File(this.outputDirectory, this.expectedName); - File snippetFile = new File(snippetDir, - this.expectedType + "." + this.templateFormat.getFileExtension()); - assertThat(snippetFile, is(this.snippet)); - } - } - - public ExpectedSnippet expectCurlRequest() { - expect("curl-request"); - return this; - } - - public ExpectedSnippet expectHttpieRequest() { - expect("httpie-request"); - return this; - } - - public ExpectedSnippet expectRequestFields() { - expect("request-fields"); - return this; - } - - public ExpectedSnippet expectResponseFields() { - expect("response-fields"); - return this; - } - - public ExpectedSnippet expectRequestHeaders() { - expect("request-headers"); - return this; - } - - public ExpectedSnippet expectResponseHeaders() { - expect("response-headers"); - return this; - } - - public ExpectedSnippet expectLinks() { - expect("links"); - return this; - } - - public ExpectedSnippet expectHttpRequest() { - expect("http-request"); - return this; - } - - public ExpectedSnippet expectHttpResponse() { - expect("http-response"); - return this; - } - - public ExpectedSnippet expectRequestParameters() { - expect("request-parameters"); - return this; - } - - public ExpectedSnippet expectPathParameters() { - expect("path-parameters"); - return this; - } - - public ExpectedSnippet expectRequestParts() { - expect("request-parts"); - return this; - } - - private ExpectedSnippet expect(String type) { - this.expectedType = type; - return this; - } - - public void withContents(Matcher matcher) { - this.snippet.withContents(matcher); - } - - public File getOutputDirectory() { - return this.outputDirectory; - } - - private final class ExpectedSnippetStatement extends Statement { - - private final Statement delegate; - - private ExpectedSnippetStatement(Statement delegate) { - this.delegate = delegate; - } - - @Override - public void evaluate() throws Throwable { - this.delegate.evaluate(); - verifySnippet(); - } - - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationTestRule.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationTestRule.java deleted file mode 100644 index 821de702d..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationTestRule.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.test; - -import java.io.File; - -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -/** - * Abstract base class for Operation-related {@link TestRule TestRules}. - * - * @author Andy Wilkinson - */ -abstract class OperationTestRule implements TestRule { - - @Override - public final Statement apply(Statement base, Description description) { - return apply(base, determineOutputDirectory(description), - determineOperationName(description)); - } - - private File determineOutputDirectory(Description description) { - return new File("build/" + description.getTestClass().getSimpleName()); - } - - private String determineOperationName(Description description) { - String operationName = description.getMethodName(); - int index = operationName.indexOf('['); - if (index > 0) { - operationName = operationName.substring(0, index); - } - return operationName; - } - - protected abstract Statement apply(Statement base, File outputDirectory, - String operationName); - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java deleted file mode 100644 index 54f4f6a52..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2012-2016 the original author or 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. - */ - -package org.springframework.restdocs.test; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.List; - -import org.hamcrest.Matcher; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -import static org.hamcrest.Matchers.allOf; -import static org.junit.Assert.assertThat; - -/** - * JUnit {@code @Rule} to capture output from System.out and System.err. - * - * @author Phillip Webb - * @author Andy Wilkinson - */ -public class OutputCapture implements TestRule { - - private CaptureOutputStream captureOut; - - private CaptureOutputStream captureErr; - - private ByteArrayOutputStream capturedOutput; - - private List> matchers = new ArrayList<>(); - - @Override - public Statement apply(final Statement base, Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - captureOutput(); - try { - base.evaluate(); - } - finally { - try { - if (!OutputCapture.this.matchers.isEmpty()) { - assertThat(getOutputAsString(), - allOf(OutputCapture.this.matchers)); - } - } - finally { - releaseOutput(); - } - } - } - }; - } - - private void captureOutput() { - this.capturedOutput = new ByteArrayOutputStream(); - this.captureOut = new CaptureOutputStream(System.out, this.capturedOutput); - this.captureErr = new CaptureOutputStream(System.err, this.capturedOutput); - System.setOut(new PrintStream(this.captureOut)); - System.setErr(new PrintStream(this.captureErr)); - } - - private String getOutputAsString() { - flush(); - return this.capturedOutput.toString(); - } - - private void releaseOutput() { - System.setOut(this.captureOut.getOriginal()); - System.setErr(this.captureErr.getOriginal()); - this.capturedOutput = null; - } - - private void flush() { - try { - this.captureOut.flush(); - this.captureErr.flush(); - } - catch (IOException ex) { - // ignore - } - } - - /** - * Verify that the output is matched by the supplied {@code matcher}. Verification is - * performed after the test method has executed. - * - * @param matcher the matcher - */ - public final void expect(Matcher matcher) { - this.matchers.add(matcher); - } - - private static final class CaptureOutputStream extends OutputStream { - - private final PrintStream original; - - private final OutputStream copy; - - private CaptureOutputStream(PrintStream original, OutputStream copy) { - this.original = original; - this.copy = copy; - } - - @Override - public void write(int b) throws IOException { - this.copy.write(b); - this.original.write(b); - this.original.flush(); - } - - @Override - public void write(byte[] b) throws IOException { - write(b, 0, b.length); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - this.copy.write(b, off, len); - this.original.write(b, off, len); - } - - private PrintStream getOriginal() { - return this.original; - } - - @Override - public void flush() throws IOException { - this.copy.flush(); - this.original.flush(); - } - - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java deleted file mode 100644 index 9d79e52b9..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ /dev/null @@ -1,458 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.test; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; - -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matcher; - -import org.springframework.http.HttpStatus; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.util.FileCopyUtils; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.RequestMethod; - -/** - * {@link Matcher Matchers} for verify the contents of generated documentation snippets. - * - * @author Andy Wilkinson - */ -public final class SnippetMatchers { - - private SnippetMatchers() { - - } - - public static SnippetMatcher snippet(TemplateFormat templateFormat) { - return new SnippetMatcher(templateFormat); - } - - public static TableMatcher tableWithHeader(TemplateFormat format, - String... headers) { - if ("adoc".equals(format.getFileExtension())) { - return new AsciidoctorTableMatcher(null, headers); - } - return new MarkdownTableMatcher(null, headers); - } - - public static TableMatcher tableWithTitleAndHeader(TemplateFormat format, - String title, String... headers) { - if ("adoc".equals(format.getFileExtension())) { - return new AsciidoctorTableMatcher(title, headers); - } - return new MarkdownTableMatcher(title, headers); - } - - public static HttpRequestMatcher httpRequest(TemplateFormat format, - RequestMethod requestMethod, String uri) { - if ("adoc".equals(format.getFileExtension())) { - return new HttpRequestMatcher(requestMethod, uri, - new AsciidoctorCodeBlockMatcher<>("http", "nowrap"), 3); - } - return new HttpRequestMatcher(requestMethod, uri, - new MarkdownCodeBlockMatcher<>("http"), 2); - } - - public static HttpResponseMatcher httpResponse(TemplateFormat format, - HttpStatus status) { - if ("adoc".equals(format.getFileExtension())) { - return new HttpResponseMatcher(status, - new AsciidoctorCodeBlockMatcher<>("http", "nowrap"), 3); - } - return new HttpResponseMatcher(status, new MarkdownCodeBlockMatcher<>("http"), 2); - } - - @SuppressWarnings({ "rawtypes" }) - public static CodeBlockMatcher codeBlock(TemplateFormat format, String language) { - if ("adoc".equals(format.getFileExtension())) { - return new AsciidoctorCodeBlockMatcher(language, null); - } - return new MarkdownCodeBlockMatcher(language); - } - - private static abstract class AbstractSnippetContentMatcher - extends BaseMatcher { - - private final TemplateFormat templateFormat; - - private List lines = new ArrayList<>(); - - protected AbstractSnippetContentMatcher(TemplateFormat templateFormat) { - this.templateFormat = templateFormat; - } - - protected void addLine(String line) { - this.lines.add(line); - } - - protected void addLine(int index, String line) { - this.lines.add(determineIndex(index), line); - } - - private int determineIndex(int index) { - if (index >= 0) { - return index; - } - return index + this.lines.size(); - } - - @Override - public boolean matches(Object item) { - return getLinesAsString().equals(item); - } - - @Override - public void describeTo(Description description) { - description.appendText(this.templateFormat.getFileExtension() + " snippet"); - description.appendText(getLinesAsString()); - } - - @Override - public void describeMismatch(Object item, Description description) { - description.appendText("was:"); - if (item instanceof String) { - description.appendText((String) item); - } - else { - description.appendValue(item); - } - } - - private String getLinesAsString() { - StringWriter writer = new StringWriter(); - Iterator iterator = this.lines.iterator(); - while (iterator.hasNext()) { - writer.append(String.format("%s", iterator.next())); - if (iterator.hasNext()) { - writer.append(String.format("%n")); - } - } - return writer.toString(); - } - } - - /** - * Base class for code block matchers. - * - * @param The type of the matcher - */ - public static class CodeBlockMatcher> - extends AbstractSnippetContentMatcher { - - protected CodeBlockMatcher(TemplateFormat templateFormat) { - super(templateFormat); - } - - @SuppressWarnings("unchecked") - public T content(String content) { - this.addLine(-1, content); - return (T) this; - } - - } - - /** - * A {@link Matcher} for an Asciidoctor code block. - * - * @param The type of the matcher - */ - public static class AsciidoctorCodeBlockMatcher> - extends CodeBlockMatcher { - - protected AsciidoctorCodeBlockMatcher(String language, String options) { - super(TemplateFormats.asciidoctor()); - this.addLine("[source," + language - + (options == null ? "" : ",options=\"" + options + "\"") + "]"); - this.addLine("----"); - this.addLine("----"); - } - - } - - /** - * A {@link Matcher} for a Markdown code block. - * - * @param The type of the matcher - */ - public static class MarkdownCodeBlockMatcher> - extends CodeBlockMatcher { - - protected MarkdownCodeBlockMatcher(String language) { - super(TemplateFormats.markdown()); - this.addLine("```" + language); - this.addLine("```"); - } - - } - - /** - * A {@link Matcher} for an HTTP request or response. - * - * @param The type of the matcher - */ - public static abstract class HttpMatcher> - extends BaseMatcher { - - private final CodeBlockMatcher delegate; - - private int headerOffset; - - protected HttpMatcher(CodeBlockMatcher delegate, int headerOffset) { - this.delegate = delegate; - this.headerOffset = headerOffset; - } - - @SuppressWarnings("unchecked") - public T header(String name, String value) { - this.delegate.addLine(this.headerOffset++, name + ": " + value); - return (T) this; - } - - @SuppressWarnings("unchecked") - public T header(String name, long value) { - this.delegate.addLine(this.headerOffset++, name + ": " + value); - return (T) this; - } - - @SuppressWarnings("unchecked") - public T content(String content) { - this.delegate.addLine(-1, content); - return (T) this; - } - - @Override - public boolean matches(Object item) { - return this.delegate.matches(item); - } - - @Override - public void describeTo(Description description) { - this.delegate.describeTo(description); - } - - } - - /** - * A {@link Matcher} for an HTTP response. - */ - public static final class HttpResponseMatcher - extends HttpMatcher { - - private HttpResponseMatcher(HttpStatus status, CodeBlockMatcher delegate, - int headerOffset) { - super(delegate, headerOffset); - this.content("HTTP/1.1 " + status.value() + " " + status.getReasonPhrase()); - this.content(""); - } - - } - - /** - * A {@link Matcher} for an HTTP request. - */ - public static final class HttpRequestMatcher extends HttpMatcher { - - private HttpRequestMatcher(RequestMethod requestMethod, String uri, - CodeBlockMatcher delegate, int headerOffset) { - super(delegate, headerOffset); - this.content(requestMethod.name() + " " + uri + " HTTP/1.1"); - this.content(""); - } - - } - - /** - * Base class for table matchers. - * - * @param The concrete type of the matcher - */ - public static abstract class TableMatcher> - extends AbstractSnippetContentMatcher { - - protected TableMatcher(TemplateFormat templateFormat) { - super(templateFormat); - } - - public abstract T row(String... entries); - - public abstract T configuration(String configuration); - - } - - /** - * A {@link Matcher} for an Asciidoctor table. - */ - public static final class AsciidoctorTableMatcher - extends TableMatcher { - - private AsciidoctorTableMatcher(String title, String... columns) { - super(TemplateFormats.asciidoctor()); - if (StringUtils.hasText(title)) { - this.addLine("." + title); - } - this.addLine("|==="); - String header = "|" + StringUtils - .collectionToDelimitedString(Arrays.asList(columns), "|"); - this.addLine(header); - this.addLine(""); - this.addLine("|==="); - } - - @Override - public AsciidoctorTableMatcher row(String... entries) { - for (String entry : entries) { - this.addLine(-1, "|" + entry); - } - this.addLine(-1, ""); - return this; - } - - @Override - public AsciidoctorTableMatcher configuration(String configuration) { - this.addLine(0, configuration); - return this; - } - - } - - /** - * A {@link Matcher} for a Markdown table. - */ - public static final class MarkdownTableMatcher - extends TableMatcher { - - private MarkdownTableMatcher(String title, String... columns) { - super(TemplateFormats.asciidoctor()); - if (StringUtils.hasText(title)) { - this.addLine(title); - this.addLine(""); - } - String header = StringUtils - .collectionToDelimitedString(Arrays.asList(columns), " | "); - this.addLine(header); - List components = new ArrayList<>(); - for (String column : columns) { - StringBuilder dashes = new StringBuilder(); - for (int i = 0; i < column.length(); i++) { - dashes.append("-"); - } - components.add(dashes.toString()); - } - this.addLine(StringUtils.collectionToDelimitedString(components, " | ")); - this.addLine(""); - } - - @Override - public MarkdownTableMatcher row(String... entries) { - this.addLine(-1, StringUtils - .collectionToDelimitedString(Arrays.asList(entries), " | ")); - return this; - } - - @Override - public MarkdownTableMatcher configuration(String configuration) { - throw new UnsupportedOperationException( - "Markdown does not support table configuration"); - } - - } - - /** - * A {@link Matcher} for a snippet file. - */ - public static final class SnippetMatcher extends BaseMatcher { - - private final TemplateFormat templateFormat; - - private Matcher expectedContents; - - private SnippetMatcher(TemplateFormat templateFormat) { - this.templateFormat = templateFormat; - } - - @Override - public boolean matches(Object item) { - if (snippetFileExists(item)) { - if (this.expectedContents != null) { - try { - return this.expectedContents.matches(read((File) item)); - } - catch (IOException e) { - return false; - } - } - return true; - } - return false; - } - - private boolean snippetFileExists(Object item) { - return item instanceof File && ((File) item).isFile(); - } - - private String read(File snippetFile) throws IOException { - return FileCopyUtils.copyToString( - new InputStreamReader(new FileInputStream(snippetFile), "UTF-8")); - } - - @Override - public void describeMismatch(Object item, Description description) { - if (!snippetFileExists(item)) { - description.appendText("The file " + item + " does not exist"); - } - else if (this.expectedContents != null) { - try { - this.expectedContents.describeMismatch(read((File) item), - description); - } - catch (IOException e) { - description - .appendText("The contents of " + item + " cound not be read"); - } - } - } - - @Override - public void describeTo(Description description) { - if (this.expectedContents != null) { - this.expectedContents.describeTo(description); - } - else { - description - .appendText(this.templateFormat.getFileExtension() + " snippet"); - } - } - - public SnippetMatcher withContents(Matcher matcher) { - this.expectedContents = matcher; - return this; - } - - } - -} diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/form-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/form-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/form-parameters-with-optional-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-optional-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/form-parameters-with-optional-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/form-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/form-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-extra-column.snippet new file mode 100644 index 000000000..fb97f89bf --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Parameter|Description|Foo + +{{#parameters}} +|{{name}} +|{{description}} +|{{foo}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-optional-column.snippet new file mode 100644 index 000000000..70847ba1d --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-optional-column.snippet @@ -0,0 +1,10 @@ +|=== +|Parameter|Optional|Description + +{{#parameters}} +|{{name}} +|{{optional}} +|{{description}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-title.snippet new file mode 100644 index 000000000..611254aa9 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/query-parameters-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Parameter|Description + +{{#parameters}} +|{{name}} +|{{description}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-body-with-language.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-body-with-language.snippet new file mode 100644 index 000000000..0429ee575 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-body-with-language.snippet @@ -0,0 +1,4 @@ +[source,{{language}},options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-extra-column.snippet new file mode 100644 index 000000000..62c9dad68 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Name|Description|Foo + +{{#cookies}} +|{{name}} +|{{description}} +|{{foo}} + +{{/cookies}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-title.snippet new file mode 100644 index 000000000..5e4a4af43 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Name|Description + +{{#cookies}} +|{{name}} +|{{description}} + +{{/cookies}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-part-body-with-language.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-part-body-with-language.snippet new file mode 100644 index 000000000..0429ee575 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-part-body-with-language.snippet @@ -0,0 +1,4 @@ +[source,{{language}},options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-body-with-language.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-body-with-language.snippet new file mode 100644 index 000000000..0429ee575 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-body-with-language.snippet @@ -0,0 +1,4 @@ +[source,{{language}},options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-extra-column.snippet new file mode 100644 index 000000000..62c9dad68 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Name|Description|Foo + +{{#cookies}} +|{{name}} +|{{description}} +|{{foo}} + +{{/cookies}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-title.snippet new file mode 100644 index 000000000..5e4a4af43 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Name|Description + +{{#cookies}} +|{{name}} +|{{description}} + +{{/cookies}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/form-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/form-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/form-parameters-with-optional-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-optional-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/form-parameters-with-optional-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/form-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/form-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-extra-column.snippet new file mode 100644 index 000000000..260502b69 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-extra-column.snippet @@ -0,0 +1,5 @@ +Parameter | Description | Foo +--------- | ----------- | --- +{{#parameters}} +{{name}} | {{description}} | {{foo}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-optional-column.snippet new file mode 100644 index 000000000..2f9cd0774 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-optional-column.snippet @@ -0,0 +1,5 @@ +Parameter | Optional | Description +--------- | -------- | ----------- +{{#parameters}} +{{name}} | {{optional}} | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-title.snippet new file mode 100644 index 000000000..b63b62353 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/query-parameters-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Parameter | Description +--------- | ----------- +{{#parameters}} +{{name}} | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-body-with-language.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-body-with-language.snippet new file mode 100644 index 000000000..f81732e96 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-body-with-language.snippet @@ -0,0 +1,3 @@ +```{{language}} +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-extra-column.snippet new file mode 100644 index 000000000..8c7416287 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-extra-column.snippet @@ -0,0 +1,5 @@ +Name | Description | Foo +---- | ----------- | --- +{{#cookies}} +{{name}} | {{description}} | {{foo}} +{{/cookies}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-title.snippet new file mode 100644 index 000000000..e5e2c8bda --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Name | Description +---- | ----------- +{{#cookies}} +{{name}} | {{description}} +{{/cookies}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-title.snippet index 24bb63fa9..ff141ad37 100644 --- a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-title.snippet +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-title.snippet @@ -1,4 +1,5 @@ {{title}} + Path | Type | Description ---- | ---- | ----------- {{#fields}} diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-part-body-with-language.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-part-body-with-language.snippet new file mode 100644 index 000000000..f81732e96 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-part-body-with-language.snippet @@ -0,0 +1,3 @@ +```{{language}} +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-body-with-language.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-body-with-language.snippet new file mode 100644 index 000000000..f81732e96 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-body-with-language.snippet @@ -0,0 +1,3 @@ +```{{language}} +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-extra-column.snippet new file mode 100644 index 000000000..8c7416287 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-extra-column.snippet @@ -0,0 +1,5 @@ +Name | Description | Foo +---- | ----------- | --- +{{#cookies}} +{{name}} | {{description}} | {{foo}} +{{/cookies}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-title.snippet new file mode 100644 index 000000000..e5e2c8bda --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Name | Description +---- | ----------- +{{#cookies}} +{{name}} | {{description}} +{{/cookies}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties index ea0f3a91c..b7150793e 100644 --- a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties +++ b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/constraints/TestConstraintDescriptions.properties @@ -1 +1 @@ -javax.validation.constraints.NotNull.description=Should not be null \ No newline at end of file +jakarta.validation.constraints.NotNull.description=Should not be null \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/multiple-snippets.snippet b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/multiple-snippets.snippet new file mode 100644 index 000000000..e69de29bb diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/SnippetConditions.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/SnippetConditions.java new file mode 100644 index 000000000..ec20b374f --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/SnippetConditions.java @@ -0,0 +1,361 @@ +/* + * Copyright 2014-2024 the original author or 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. + */ + +package org.springframework.restdocs.testfixtures; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import org.assertj.core.api.Condition; +import org.assertj.core.description.Description; + +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * {@link Condition Conditions} for verify the contents of generated documentation + * snippets. + * + * @author Andy Wilkinson + */ +public final class SnippetConditions { + + private SnippetConditions() { + + } + + public static TableCondition tableWithHeader(TemplateFormat format, String... headers) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorTableCondition(null, headers); + } + return new MarkdownTableCondition(null, headers); + } + + public static TableCondition tableWithTitleAndHeader(TemplateFormat format, String title, String... headers) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorTableCondition(title, headers); + } + return new MarkdownTableCondition(title, headers); + } + + public static HttpRequestCondition httpRequest(TemplateFormat format, RequestMethod requestMethod, String uri) { + if ("adoc".equals(format.getFileExtension())) { + return new HttpRequestCondition(requestMethod, uri, new AsciidoctorCodeBlockCondition<>("http", "nowrap"), + 3); + } + return new HttpRequestCondition(requestMethod, uri, new MarkdownCodeBlockCondition<>("http"), 2); + } + + public static HttpResponseCondition httpResponse(TemplateFormat format, HttpStatus status) { + if ("adoc".equals(format.getFileExtension())) { + return new HttpResponseCondition(status, new AsciidoctorCodeBlockCondition<>("http", "nowrap"), 3); + } + return new HttpResponseCondition(status, new MarkdownCodeBlockCondition<>("http"), 2); + } + + public static HttpResponseCondition httpResponse(TemplateFormat format, Integer responseStatusCode, + String responseStatusReason) { + if ("adoc".equals(format.getFileExtension())) { + return new HttpResponseCondition(responseStatusCode, responseStatusReason, + new AsciidoctorCodeBlockCondition<>("http", "nowrap"), 3); + } + return new HttpResponseCondition(responseStatusCode, responseStatusReason, + new MarkdownCodeBlockCondition<>("http"), 2); + } + + @SuppressWarnings("rawtypes") + public static CodeBlockCondition codeBlock(TemplateFormat format, String language) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorCodeBlockCondition(language, null); + } + return new MarkdownCodeBlockCondition(language); + } + + @SuppressWarnings("rawtypes") + public static CodeBlockCondition codeBlock(TemplateFormat format, String language, String options) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorCodeBlockCondition(language, options); + } + return new MarkdownCodeBlockCondition(language); + } + + private abstract static class AbstractSnippetContentCondition extends Condition { + + private List lines = new ArrayList<>(); + + protected AbstractSnippetContentCondition() { + as(new Description() { + + @Override + public String value() { + return getLinesAsString(); + } + + }); + } + + protected void addLine(String line) { + this.lines.add(line); + } + + protected void addLine(int index, String line) { + this.lines.add(determineIndex(index), line); + } + + private int determineIndex(int index) { + if (index >= 0) { + return index; + } + return index + this.lines.size(); + } + + @Override + public boolean matches(String content) { + return getLinesAsString().equals(content); + } + + private String getLinesAsString() { + StringWriter writer = new StringWriter(); + Iterator iterator = this.lines.iterator(); + while (iterator.hasNext()) { + writer.append(String.format("%s", iterator.next())); + if (iterator.hasNext()) { + writer.append(String.format("%n")); + } + } + return writer.toString(); + } + + } + + /** + * Base class for code block Conditions. + * + * @param the type of the Condition + */ + public static class CodeBlockCondition> extends AbstractSnippetContentCondition { + + @SuppressWarnings("unchecked") + public T withContent(String content) { + this.addLine(-1, content); + return (T) this; + } + + } + + /** + * A {@link Condition} for an Asciidoctor code block. + * + * @param the type of the Condition + */ + public static class AsciidoctorCodeBlockCondition> + extends CodeBlockCondition { + + protected AsciidoctorCodeBlockCondition(String language, String options) { + this.addLine("[source" + ((language != null) ? "," + language : "") + + ((options != null) ? ",options=\"" + options + "\"" : "") + "]"); + this.addLine("----"); + this.addLine("----"); + } + + } + + /** + * A {@link Condition} for a Markdown code block. + * + * @param the type of the Condition + */ + public static class MarkdownCodeBlockCondition> + extends CodeBlockCondition { + + protected MarkdownCodeBlockCondition(String language) { + this.addLine("```" + ((language != null) ? language : "")); + this.addLine("```"); + } + + } + + /** + * A {@link Condition} for an HTTP request or response. + * + * @param the type of the Condition + */ + public abstract static class HttpCondition> extends Condition { + + private final CodeBlockCondition delegate; + + private int headerOffset; + + protected HttpCondition(CodeBlockCondition delegate, int headerOffset) { + this.delegate = delegate; + this.headerOffset = headerOffset; + } + + @SuppressWarnings("unchecked") + public T header(String name, String value) { + this.delegate.addLine(this.headerOffset++, name + ": " + value); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T header(String name, long value) { + this.delegate.addLine(this.headerOffset++, name + ": " + value); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T content(String content) { + this.delegate.addLine(-1, content); + return (T) this; + } + + @Override + public boolean matches(String item) { + return this.delegate.matches(item); + } + + } + + /** + * A {@link Condition} for an HTTP response. + */ + public static final class HttpResponseCondition extends HttpCondition { + + private HttpResponseCondition(HttpStatus status, CodeBlockCondition delegate, int headerOffset) { + super(delegate, headerOffset); + this.content("HTTP/1.1 " + status.value() + " " + status.getReasonPhrase()); + this.content(""); + } + + private HttpResponseCondition(int responseStatusCode, String responseStatusReason, + CodeBlockCondition delegate, int headerOffset) { + super(delegate, headerOffset); + this.content("HTTP/1.1 " + responseStatusCode + " " + responseStatusReason); + this.content(""); + } + + } + + /** + * A {@link Condition} for an HTTP request. + */ + public static final class HttpRequestCondition extends HttpCondition { + + private HttpRequestCondition(RequestMethod requestMethod, String uri, CodeBlockCondition delegate, + int headerOffset) { + super(delegate, headerOffset); + this.content(requestMethod.name() + " " + uri + " HTTP/1.1"); + this.content(""); + } + + } + + /** + * Base class for table Conditions. + * + * @param the concrete type of the Condition + */ + public abstract static class TableCondition> extends AbstractSnippetContentCondition { + + public abstract T row(String... entries); + + public abstract T configuration(String configuration); + + } + + /** + * A {@link Condition} for an Asciidoctor table. + */ + public static final class AsciidoctorTableCondition extends TableCondition { + + private AsciidoctorTableCondition(String title, String... columns) { + if (StringUtils.hasText(title)) { + this.addLine("." + title); + } + this.addLine("|==="); + String header = "|" + StringUtils.collectionToDelimitedString(Arrays.asList(columns), "|"); + this.addLine(header); + this.addLine(""); + this.addLine("|==="); + } + + @Override + public AsciidoctorTableCondition row(String... entries) { + for (String entry : entries) { + this.addLine(-1, "|" + escapeEntry(entry)); + } + this.addLine(-1, ""); + return this; + } + + private String escapeEntry(String entry) { + if (entry.startsWith("`") && entry.endsWith("`")) { + return "`+" + entry.substring(1, entry.length() - 1) + "+`"; + } + return entry; + } + + @Override + public AsciidoctorTableCondition configuration(String configuration) { + this.addLine(0, configuration); + return this; + } + + } + + /** + * A {@link Condition} for a Markdown table. + */ + public static final class MarkdownTableCondition extends TableCondition { + + private MarkdownTableCondition(String title, String... columns) { + if (StringUtils.hasText(title)) { + this.addLine(title); + this.addLine(""); + } + String header = StringUtils.collectionToDelimitedString(Arrays.asList(columns), " | "); + this.addLine(header); + List components = new ArrayList<>(); + for (String column : columns) { + StringBuilder dashes = new StringBuilder(); + for (int i = 0; i < column.length(); i++) { + dashes.append("-"); + } + components.add(dashes.toString()); + } + this.addLine(StringUtils.collectionToDelimitedString(components, " | ")); + this.addLine(""); + } + + @Override + public MarkdownTableCondition row(String... entries) { + this.addLine(-1, StringUtils.collectionToDelimitedString(Arrays.asList(entries), " | ")); + return this; + } + + @Override + public MarkdownTableCondition configuration(String configuration) { + throw new UnsupportedOperationException("Markdown does not support table configuration"); + } + + } + +} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/AssertableSnippets.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/AssertableSnippets.java new file mode 100644 index 000000000..fdab049e1 --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/AssertableSnippets.java @@ -0,0 +1,679 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.UnaryOperator; + +import org.assertj.core.api.AbstractStringAssert; +import org.assertj.core.api.AssertProvider; +import org.assertj.core.api.Assertions; + +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.util.StringUtils; + +/** + * AssertJ {@link AssertProvider} for asserting that the generated snippets are correct. + * + * @author Andy Wilkinson + */ +public class AssertableSnippets { + + private final File outputDirectory; + + private final String operationName; + + private final TemplateFormat templateFormat; + + AssertableSnippets(File outputDirectory, String operationName, TemplateFormat templateFormat) { + this.outputDirectory = outputDirectory; + this.operationName = operationName; + this.templateFormat = templateFormat; + } + + public File named(String name) { + return getSnippetFile(name); + } + + private File getSnippetFile(String name) { + File snippetDir = new File(this.outputDirectory, this.operationName); + return new File(snippetDir, name + "." + this.templateFormat.getFileExtension()); + } + + public CodeBlockSnippetAssertProvider curlRequest() { + return new CodeBlockSnippetAssertProvider("curl-request"); + } + + public TableSnippetAssertProvider formParameters() { + return new TableSnippetAssertProvider("form-parameters"); + } + + public CodeBlockSnippetAssertProvider httpieRequest() { + return new CodeBlockSnippetAssertProvider("httpie-request"); + } + + public HttpRequestSnippetAssertProvider httpRequest() { + return new HttpRequestSnippetAssertProvider("http-request"); + } + + public HttpResponseSnippetAssertProvider httpResponse() { + return new HttpResponseSnippetAssertProvider("http-response"); + } + + public TableSnippetAssertProvider links() { + return new TableSnippetAssertProvider("links"); + } + + public TableSnippetAssertProvider pathParameters() { + return new TableSnippetAssertProvider("path-parameters"); + } + + public TableSnippetAssertProvider queryParameters() { + return new TableSnippetAssertProvider("query-parameters"); + } + + public CodeBlockSnippetAssertProvider requestBody() { + return new CodeBlockSnippetAssertProvider("request-body"); + } + + public CodeBlockSnippetAssertProvider requestBody(String suffix) { + return new CodeBlockSnippetAssertProvider("request-body-%s".formatted(suffix)); + } + + public TableSnippetAssertProvider requestCookies() { + return new TableSnippetAssertProvider("request-cookies"); + } + + public TableSnippetAssertProvider requestCookies(String suffix) { + return new TableSnippetAssertProvider("request-cookies-%s".formatted(suffix)); + } + + public TableSnippetAssertProvider requestFields() { + return new TableSnippetAssertProvider("request-fields"); + } + + public TableSnippetAssertProvider requestFields(String suffix) { + return new TableSnippetAssertProvider("request-fields-%s".formatted(suffix)); + } + + public TableSnippetAssertProvider requestHeaders() { + return new TableSnippetAssertProvider("request-headers"); + } + + public TableSnippetAssertProvider requestHeaders(String suffix) { + return new TableSnippetAssertProvider("request-headers-%s".formatted(suffix)); + } + + public CodeBlockSnippetAssertProvider requestPartBody(String partName) { + return new CodeBlockSnippetAssertProvider("request-part-%s-body".formatted(partName)); + } + + public CodeBlockSnippetAssertProvider requestPartBody(String partName, String suffix) { + return new CodeBlockSnippetAssertProvider("request-part-%s-body-%s".formatted(partName, suffix)); + } + + public TableSnippetAssertProvider requestPartFields(String partName) { + return new TableSnippetAssertProvider("request-part-%s-fields".formatted(partName)); + } + + public TableSnippetAssertProvider requestPartFields(String partName, String suffix) { + return new TableSnippetAssertProvider("request-part-%s-fields-%s".formatted(partName, suffix)); + } + + public TableSnippetAssertProvider requestParts() { + return new TableSnippetAssertProvider("request-parts"); + } + + public CodeBlockSnippetAssertProvider responseBody() { + return new CodeBlockSnippetAssertProvider("response-body"); + } + + public CodeBlockSnippetAssertProvider responseBody(String suffix) { + return new CodeBlockSnippetAssertProvider("response-body-%s".formatted(suffix)); + } + + public TableSnippetAssertProvider responseCookies() { + return new TableSnippetAssertProvider("response-cookies"); + } + + public TableSnippetAssertProvider responseFields() { + return new TableSnippetAssertProvider("response-fields"); + } + + public TableSnippetAssertProvider responseFields(String suffix) { + return new TableSnippetAssertProvider("response-fields-%s".formatted(suffix)); + } + + public TableSnippetAssertProvider responseHeaders() { + return new TableSnippetAssertProvider("response-headers"); + } + + public final class TableSnippetAssertProvider implements AssertProvider { + + private final String snippetName; + + private TableSnippetAssertProvider(String snippetName) { + this.snippetName = snippetName; + } + + @Override + public TableSnippetAssert assertThat() { + try { + String content = Files + .readString(new File(AssertableSnippets.this.outputDirectory, AssertableSnippets.this.operationName + + "/" + this.snippetName + "." + AssertableSnippets.this.templateFormat.getFileExtension()) + .toPath()); + return new TableSnippetAssert(content); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + } + + public final class TableSnippetAssert extends AbstractStringAssert { + + private TableSnippetAssert(String actual) { + super(actual, TableSnippetAssert.class); + } + + public void isTable(UnaryOperator> tableOperator) { + Table table = tableOperator + .apply(AssertableSnippets.this.templateFormat.equals(TemplateFormats.asciidoctor()) + ? new AsciidoctorTable() : new MarkdownTable()); + table.getLinesAsString(); + Assertions.assertThat(this.actual).isEqualTo(table.getLinesAsString()); + } + + } + + public abstract class Table> extends SnippetContent { + + public abstract T withHeader(String... columns); + + public abstract T withTitleAndHeader(String title, String... columns); + + public abstract T row(String... entries); + + public abstract T configuration(String string); + + } + + private final class AsciidoctorTable extends Table { + + @Override + public AsciidoctorTable withHeader(String... columns) { + return withTitleAndHeader("", columns); + } + + @Override + public AsciidoctorTable withTitleAndHeader(String title, String... columns) { + if (!title.isBlank()) { + this.addLine("." + title); + } + this.addLine("|==="); + String header = "|" + StringUtils.collectionToDelimitedString(Arrays.asList(columns), "|"); + this.addLine(header); + this.addLine(""); + this.addLine("|==="); + return this; + } + + @Override + public AsciidoctorTable row(String... entries) { + for (String entry : entries) { + this.addLine(-1, "|" + escapeEntry(entry)); + } + this.addLine(-1, ""); + return this; + } + + private String escapeEntry(String entry) { + entry = entry.replace("|", "\\|"); + if (entry.startsWith("`") && entry.endsWith("`")) { + return "`+" + entry.substring(1, entry.length() - 1) + "+`"; + } + return entry; + } + + @Override + public AsciidoctorTable configuration(String configuration) { + this.addLine(0, configuration); + return this; + } + + } + + private final class MarkdownTable extends Table { + + @Override + public MarkdownTable withHeader(String... columns) { + return withTitleAndHeader("", columns); + } + + @Override + public MarkdownTable withTitleAndHeader(String title, String... columns) { + if (StringUtils.hasText(title)) { + this.addLine(title); + this.addLine(""); + } + String header = StringUtils.collectionToDelimitedString(Arrays.asList(columns), " | "); + this.addLine(header); + List components = new ArrayList<>(); + for (String column : columns) { + StringBuilder dashes = new StringBuilder(); + for (int i = 0; i < column.length(); i++) { + dashes.append("-"); + } + components.add(dashes.toString()); + } + this.addLine(StringUtils.collectionToDelimitedString(components, " | ")); + this.addLine(""); + return this; + } + + @Override + public MarkdownTable row(String... entries) { + this.addLine(-1, StringUtils.collectionToDelimitedString(Arrays.asList(entries), " | ")); + return this; + } + + @Override + public MarkdownTable configuration(String configuration) { + throw new UnsupportedOperationException("Markdown tables do not support configuration"); + } + + } + + public final class CodeBlockSnippetAssertProvider implements AssertProvider { + + private final String snippetName; + + private CodeBlockSnippetAssertProvider(String snippetName) { + this.snippetName = snippetName; + } + + @Override + public CodeBlockSnippetAssert assertThat() { + try { + String content = Files + .readString(new File(AssertableSnippets.this.outputDirectory, AssertableSnippets.this.operationName + + "/" + this.snippetName + "." + AssertableSnippets.this.templateFormat.getFileExtension()) + .toPath()); + return new CodeBlockSnippetAssert(content); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + } + + public final class CodeBlockSnippetAssert extends AbstractStringAssert { + + private CodeBlockSnippetAssert(String actual) { + super(actual, CodeBlockSnippetAssert.class); + } + + public void isCodeBlock(UnaryOperator> codeBlockOperator) { + CodeBlock codeBlock = codeBlockOperator + .apply(AssertableSnippets.this.templateFormat.equals(TemplateFormats.asciidoctor()) + ? new AsciidoctorCodeBlock() : new MarkdownCodeBlock()); + Assertions.assertThat(this.actual).isEqualTo(codeBlock.getLinesAsString()); + } + + } + + public abstract class CodeBlock> extends SnippetContent { + + public abstract T withLanguage(String language); + + public abstract T withOptions(String options); + + public abstract T withLanguageAndOptions(String language, String options); + + public abstract T content(String string); + + } + + private final class AsciidoctorCodeBlock extends CodeBlock { + + @Override + public AsciidoctorCodeBlock withLanguage(String language) { + addLine("[source,%s]".formatted(language)); + return this; + } + + @Override + public AsciidoctorCodeBlock withOptions(String options) { + addLine("[source,options=\"%s\"]".formatted(options)); + return this; + } + + @Override + public AsciidoctorCodeBlock withLanguageAndOptions(String language, String options) { + addLine("[source,%s,options=\"%s\"]".formatted(language, options)); + return this; + } + + @Override + public AsciidoctorCodeBlock content(String content) { + addLine("----"); + addLine(content); + addLine("----"); + return this; + } + + } + + private final class MarkdownCodeBlock extends CodeBlock { + + @Override + public MarkdownCodeBlock withLanguage(String language) { + addLine("```%s".formatted(language)); + return this; + } + + @Override + public MarkdownCodeBlock withOptions(String options) { + addLine("```"); + return this; + } + + @Override + public MarkdownCodeBlock withLanguageAndOptions(String language, String options) { + addLine("```%s".formatted(language)); + return this; + } + + @Override + public MarkdownCodeBlock content(String content) { + addLine(content); + addLine("```"); + return this; + } + + } + + public final class HttpRequestSnippetAssertProvider implements AssertProvider { + + private final String snippetName; + + private HttpRequestSnippetAssertProvider(String snippetName) { + this.snippetName = snippetName; + } + + @Override + public HttpRequestSnippetAssert assertThat() { + try { + String content = Files + .readString(new File(AssertableSnippets.this.outputDirectory, AssertableSnippets.this.operationName + + "/" + this.snippetName + "." + AssertableSnippets.this.templateFormat.getFileExtension()) + .toPath()); + return new HttpRequestSnippetAssert(content); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + } + + public final class HttpRequestSnippetAssert extends AbstractStringAssert { + + private HttpRequestSnippetAssert(String actual) { + super(actual, HttpRequestSnippetAssert.class); + } + + public void isHttpRequest(UnaryOperator> operator) { + HttpRequest codeBlock = operator + .apply(AssertableSnippets.this.templateFormat.equals(TemplateFormats.asciidoctor()) + ? new AsciidoctorHttpRequest() : new MarkdownHttpRequest()); + Assertions.assertThat(this.actual).isEqualTo(codeBlock.getLinesAsString()); + } + + } + + public abstract class HttpRequest> extends SnippetContent { + + public T get(String uri) { + return request("GET", uri); + } + + public T post(String uri) { + return request("POST", uri); + } + + public T put(String uri) { + return request("PUT", uri); + } + + public T patch(String uri) { + return request("PATCH", uri); + } + + public T delete(String uri) { + return request("DELETE", uri); + } + + protected abstract T request(String method, String uri); + + public abstract T header(String name, Object value); + + @SuppressWarnings("unchecked") + public T content(String content) { + addLine(-1, content); + return (T) this; + } + + } + + private final class AsciidoctorHttpRequest extends HttpRequest { + + private int headerOffset = 3; + + @Override + protected AsciidoctorHttpRequest request(String method, String uri) { + addLine("[source,http,options=\"nowrap\"]"); + addLine("----"); + addLine("%s %s HTTP/1.1".formatted(method, uri)); + addLine(""); + addLine("----"); + return this; + } + + @Override + public AsciidoctorHttpRequest header(String name, Object value) { + addLine(this.headerOffset++, "%s: %s".formatted(name, value)); + return this; + } + + } + + private final class MarkdownHttpRequest extends HttpRequest { + + private int headerOffset = 2; + + @Override + public MarkdownHttpRequest request(String method, String uri) { + addLine("```http"); + addLine("%s %s HTTP/1.1".formatted(method, uri)); + addLine(""); + addLine("```"); + return this; + } + + @Override + public MarkdownHttpRequest header(String name, Object value) { + addLine(this.headerOffset++, "%s: %s".formatted(name, value)); + return this; + } + + } + + public final class HttpResponseSnippetAssertProvider implements AssertProvider { + + private final String snippetName; + + private HttpResponseSnippetAssertProvider(String snippetName) { + this.snippetName = snippetName; + } + + @Override + public HttpResponseSnippetAssert assertThat() { + try { + String content = Files + .readString(new File(AssertableSnippets.this.outputDirectory, AssertableSnippets.this.operationName + + "/" + this.snippetName + "." + AssertableSnippets.this.templateFormat.getFileExtension()) + .toPath()); + return new HttpResponseSnippetAssert(content); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + } + + public final class HttpResponseSnippetAssert extends AbstractStringAssert { + + private HttpResponseSnippetAssert(String actual) { + super(actual, HttpResponseSnippetAssert.class); + } + + public void isHttpResponse(UnaryOperator> operator) { + HttpResponse httpResponse = operator + .apply(AssertableSnippets.this.templateFormat.equals(TemplateFormats.asciidoctor()) + ? new AsciidoctorHttpResponse() : new MarkdownHttpResponse()); + Assertions.assertThat(this.actual).isEqualTo(httpResponse.getLinesAsString()); + } + + } + + public abstract class HttpResponse> extends SnippetContent { + + public T ok() { + return status("200 OK"); + } + + public T badRequest() { + return status("400 Bad Request"); + } + + public T status(int status) { + return status("%d ".formatted(status)); + } + + protected abstract T status(String status); + + public abstract T header(String name, Object value); + + @SuppressWarnings("unchecked") + public T content(String content) { + addLine(-1, content); + return (T) this; + } + + } + + private final class AsciidoctorHttpResponse extends HttpResponse { + + private int headerOffset = 3; + + @Override + protected AsciidoctorHttpResponse status(String status) { + addLine("[source,http,options=\"nowrap\"]"); + addLine("----"); + addLine("HTTP/1.1 %s".formatted(status)); + addLine(""); + addLine("----"); + return this; + } + + @Override + public AsciidoctorHttpResponse header(String name, Object value) { + addLine(this.headerOffset++, "%s: %s".formatted(name, value)); + return this; + } + + } + + private final class MarkdownHttpResponse extends HttpResponse { + + private int headerOffset = 2; + + @Override + public MarkdownHttpResponse status(String status) { + addLine("```http"); + addLine("HTTP/1.1 %s".formatted(status)); + addLine(""); + addLine("```"); + return this; + } + + @Override + public MarkdownHttpResponse header(String name, Object value) { + addLine(this.headerOffset++, "%s: %s".formatted(name, value)); + return this; + } + + } + + private static class SnippetContent { + + private List lines = new ArrayList<>(); + + protected void addLine(String line) { + this.lines.add(line); + } + + protected void addLine(int index, String line) { + this.lines.add(determineIndex(index), line); + } + + private int determineIndex(int index) { + if (index >= 0) { + return index; + } + return index + this.lines.size(); + } + + protected String getLinesAsString() { + StringWriter writer = new StringWriter(); + Iterator iterator = this.lines.iterator(); + while (iterator.hasNext()) { + writer.append(String.format("%s", iterator.next())); + if (iterator.hasNext()) { + writer.append(String.format("%n")); + } + } + return writer.toString(); + } + + } + +} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/CapturedOutput.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/CapturedOutput.java new file mode 100644 index 000000000..d3aaed82c --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/CapturedOutput.java @@ -0,0 +1,63 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +/** + * Provides access to {@link System#out System.out} and {@link System#err System.err} + * output that has been captured. + * + * @author Madhura Bhave + * @author Phillip Webb + * @author Andy Wilkinson + */ +public interface CapturedOutput extends CharSequence { + + @Override + default int length() { + return toString().length(); + } + + @Override + default char charAt(int index) { + return toString().charAt(index); + } + + @Override + default CharSequence subSequence(int start, int end) { + return toString().subSequence(start, end); + } + + /** + * Return all content (both {@link System#out System.out} and {@link System#err + * System.err}) in the order that it was captured. + * @return all captured output + */ + String getAll(); + + /** + * Return {@link System#out System.out} content in the order that it was captured. + * @return {@link System#out System.out} captured output + */ + String getOut(); + + /** + * Return {@link System#err System.err} content in the order that it was captured. + * @return {@link System#err System.err} captured output + */ + String getErr(); + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OperationBuilder.java similarity index 76% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java rename to spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OperationBuilder.java index c32958ea2..97c9ebcdd 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OperationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,21 +14,22 @@ * limitations under the License. */ -package org.springframework.restdocs.test; +package org.springframework.restdocs.testfixtures.jupiter; import java.io.File; import java.net.URI; import java.util.ArrayList; -import java.util.Collections; +import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; - -import org.junit.runners.model.Statement; +import java.util.Set; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.mustache.Mustache; @@ -39,7 +40,8 @@ import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; +import org.springframework.restdocs.operation.ResponseCookie; import org.springframework.restdocs.operation.StandardOperation; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory; import org.springframework.restdocs.snippet.StandardWriterResolver; @@ -55,21 +57,27 @@ * * @author Andy Wilkinson */ -public class OperationBuilder extends OperationTestRule { +public class OperationBuilder { private final Map attributes = new HashMap<>(); - private OperationResponseBuilder responseBuilder; - - private String name; + private final File outputDirectory; - private File outputDirectory; + private final String name; private final TemplateFormat templateFormat; + private OperationResponseBuilder responseBuilder; + private OperationRequestBuilder requestBuilder; - public OperationBuilder(TemplateFormat templateFormat) { + OperationBuilder(File outputDirectory, String name) { + this(outputDirectory, name, null); + } + + OperationBuilder(File outputDirectory, String name, TemplateFormat templateFormat) { + this.outputDirectory = outputDirectory; + this.name = name; this.templateFormat = templateFormat; } @@ -88,36 +96,22 @@ public OperationBuilder attribute(String name, Object value) { return this; } - private void prepare(String operationName, File outputDirectory) { - this.name = operationName; - this.outputDirectory = outputDirectory; - this.requestBuilder = null; - this.requestBuilder = null; - this.attributes.clear(); - } - public Operation build() { if (this.attributes.get(TemplateEngine.class.getName()) == null) { Map templateContext = new HashMap<>(); - templateContext.put("tableCellContent", - new AsciidoctorTableCellContentLambda()); + templateContext.put("tableCellContent", new AsciidoctorTableCellContentLambda()); this.attributes.put(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - new StandardTemplateResourceResolver(this.templateFormat), + new MustacheTemplateEngine(new StandardTemplateResourceResolver(this.templateFormat), Mustache.compiler().escapeHTML(false), templateContext)); } RestDocumentationContext context = createContext(); this.attributes.put(RestDocumentationContext.class.getName(), context); - this.attributes.put(WriterResolver.class.getName(), - new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolverFactory(), "UTF-8", - this.templateFormat)); + this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( + new RestDocumentationContextPlaceholderResolverFactory(), "UTF-8", this.templateFormat)); return new StandardOperation(this.name, - (this.requestBuilder == null - ? new OperationRequestBuilder("http://localhost/").buildRequest() + ((this.requestBuilder == null) ? new OperationRequestBuilder("http://localhost/").buildRequest() : this.requestBuilder.buildRequest()), - this.responseBuilder == null - ? new OperationResponseBuilder().buildResponse() + (this.responseBuilder == null) ? new OperationResponseBuilder().buildResponse() : this.responseBuilder.buildResponse(), this.attributes); } @@ -130,12 +124,6 @@ private RestDocumentationContext createContext() { return context; } - @Override - public Statement apply(Statement base, File outputDirectory, String operationName) { - prepare(operationName, outputDirectory); - return base; - } - /** * Basic builder API for creating an {@link OperationRequest}. */ @@ -149,10 +137,10 @@ public final class OperationRequestBuilder { private HttpHeaders headers = new HttpHeaders(); - private Parameters parameters = new Parameters(); - private List partBuilders = new ArrayList<>(); + private Collection cookies = new ArrayList<>(); + private OperationRequestBuilder(String uri) { this.requestUri = URI.create(uri); } @@ -162,8 +150,8 @@ private OperationRequest buildRequest() { for (OperationRequestPartBuilder builder : this.partBuilders) { parts.add(builder.buildPart()); } - return new OperationRequestFactory().create(this.requestUri, this.method, - this.content, this.headers, this.parameters, parts); + return new OperationRequestFactory().create(this.requestUri, this.method, this.content, this.headers, parts, + this.cookies); } public Operation build() { @@ -185,30 +173,22 @@ public OperationRequestBuilder content(byte[] content) { return this; } - public OperationRequestBuilder param(String name, String... values) { - if (values.length > 0) { - for (String value : values) { - this.parameters.add(name, value); - } - } - else { - this.parameters.put(name, Collections.emptyList()); - } - return this; - } - public OperationRequestBuilder header(String name, String value) { this.headers.add(name, value); return this; } public OperationRequestPartBuilder part(String name, byte[] content) { - OperationRequestPartBuilder partBuilder = new OperationRequestPartBuilder( - name, content); + OperationRequestPartBuilder partBuilder = new OperationRequestPartBuilder(name, content); this.partBuilders.add(partBuilder); return partBuilder; } + public OperationRequestBuilder cookie(String name, String value) { + this.cookies.add(new RequestCookie(name, value)); + return this; + } + /** * Basic builder API for creating an {@link OperationRequestPart}. */ @@ -227,8 +207,7 @@ private OperationRequestPartBuilder(String name, byte[] content) { this.content = content; } - public OperationRequestPartBuilder submittedFileName( - String submittedFileName) { + public OperationRequestPartBuilder submittedFileName(String submittedFileName) { this.submittedFileName = submittedFileName; return this; } @@ -242,15 +221,17 @@ public Operation build() { } private OperationRequestPart buildPart() { - return new OperationRequestPartFactory().create(this.name, - this.submittedFileName, this.content, this.headers); + return new OperationRequestPartFactory().create(this.name, this.submittedFileName, this.content, + this.headers); } public OperationRequestPartBuilder header(String name, String value) { this.headers.add(name, value); return this; } + } + } /** @@ -258,19 +239,20 @@ public OperationRequestPartBuilder header(String name, String value) { */ public final class OperationResponseBuilder { - private HttpStatus status = HttpStatus.OK; + private HttpStatusCode status = HttpStatus.OK; private HttpHeaders headers = new HttpHeaders(); + private Set cookies = new HashSet<>(); + private byte[] content = new byte[0]; private OperationResponse buildResponse() { - return new OperationResponseFactory().create(this.status, this.headers, - this.content); + return new OperationResponseFactory().create(this.status, this.headers, this.content, this.cookies); } - public OperationResponseBuilder status(int status) { - this.status = HttpStatus.valueOf(status); + public OperationResponseBuilder status(HttpStatusCode status) { + this.status = status; return this; } @@ -279,6 +261,11 @@ public OperationResponseBuilder header(String name, String value) { return this; } + public OperationResponseBuilder cookie(String name, String value) { + this.cookies.add(new ResponseCookie(name, value)); + return this; + } + public OperationResponseBuilder content(byte[] content) { this.content = content; return this; diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCapture.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCapture.java new file mode 100644 index 000000000..385dd49cf --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCapture.java @@ -0,0 +1,270 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.springframework.util.Assert; + +/** + * Provides support for capturing {@link System#out System.out} and {@link System#err + * System.err}. + * + * @author Madhura Bhave + * @author Phillip Webb + * @author Andy Wilkinson + */ +class OutputCapture implements CapturedOutput { + + private final Deque systemCaptures = new ArrayDeque<>(); + + /** + * Push a new system capture session onto the stack. + */ + final void push() { + this.systemCaptures.addLast(new SystemCapture()); + } + + /** + * Pop the last system capture session from the stack. + */ + final void pop() { + this.systemCaptures.removeLast().release(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof CharSequence) { + return getAll().equals(obj.toString()); + } + return false; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public String toString() { + return getAll(); + } + + /** + * Return all content (both {@link System#out System.out} and {@link System#err + * System.err}) in the order that it was captured. + * @return all captured output + */ + @Override + public String getAll() { + return get((type) -> true); + } + + /** + * Return {@link System#out System.out} content in the order that it was captured. + * @return {@link System#out System.out} captured output + */ + @Override + public String getOut() { + return get(Type.OUT::equals); + } + + /** + * Return {@link System#err System.err} content in the order that it was captured. + * @return {@link System#err System.err} captured output + */ + @Override + public String getErr() { + return get(Type.ERR::equals); + } + + /** + * Resets the current capture session, clearing its captured output. + */ + void reset() { + this.systemCaptures.peek().reset(); + } + + private String get(Predicate filter) { + Assert.state(!this.systemCaptures.isEmpty(), + "No system captures found. Please check your output capture registration."); + StringBuilder builder = new StringBuilder(); + for (SystemCapture systemCapture : this.systemCaptures) { + systemCapture.append(builder, filter); + } + return builder.toString(); + } + + /** + * A capture session that captures {@link System#out System.out} and {@link System#err + * System.err}. + */ + private static class SystemCapture { + + private final PrintStreamCapture out; + + private final PrintStreamCapture err; + + private final Object monitor = new Object(); + + private final List capturedStrings = new ArrayList<>(); + + SystemCapture() { + this.out = new PrintStreamCapture(System.out, this::captureOut); + this.err = new PrintStreamCapture(System.err, this::captureErr); + System.setOut(this.out); + System.setErr(this.err); + } + + void release() { + System.setOut(this.out.getParent()); + System.setErr(this.err.getParent()); + } + + private void captureOut(String string) { + synchronized (this.monitor) { + this.capturedStrings.add(new CapturedString(Type.OUT, string)); + } + } + + private void captureErr(String string) { + synchronized (this.monitor) { + this.capturedStrings.add(new CapturedString(Type.ERR, string)); + } + } + + void append(StringBuilder builder, Predicate filter) { + synchronized (this.monitor) { + for (CapturedString stringCapture : this.capturedStrings) { + if (filter.test(stringCapture.getType())) { + builder.append(stringCapture); + } + } + } + } + + void reset() { + synchronized (this.monitor) { + this.capturedStrings.clear(); + } + } + + } + + /** + * A {@link PrintStream} implementation that captures written strings. + */ + private static class PrintStreamCapture extends PrintStream { + + private final PrintStream parent; + + PrintStreamCapture(PrintStream parent, Consumer copy) { + super(new OutputStreamCapture(getSystemStream(parent), copy)); + this.parent = parent; + } + + PrintStream getParent() { + return this.parent; + } + + private static PrintStream getSystemStream(PrintStream printStream) { + while (printStream instanceof PrintStreamCapture printStreamCapture) { + printStream = printStreamCapture.getParent(); + } + return printStream; + } + + } + + /** + * An {@link OutputStream} implementation that captures written strings. + */ + private static class OutputStreamCapture extends OutputStream { + + private final PrintStream systemStream; + + private final Consumer copy; + + OutputStreamCapture(PrintStream systemStream, Consumer copy) { + this.systemStream = systemStream; + this.copy = copy; + } + + @Override + public void write(int b) throws IOException { + write(new byte[] { (byte) (b & 0xFF) }); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + this.copy.accept(new String(b, off, len)); + this.systemStream.write(b, off, len); + } + + @Override + public void flush() throws IOException { + this.systemStream.flush(); + } + + } + + /** + * A captured string that forms part of the full output. + */ + private static class CapturedString { + + private final Type type; + + private final String string; + + CapturedString(Type type, String string) { + this.type = type; + this.string = string; + } + + Type getType() { + return this.type; + } + + @Override + public String toString() { + return this.string; + } + + } + + /** + * Types of content that can be captured. + */ + private enum Type { + + OUT, ERR + + } + +} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCaptureExtension.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCaptureExtension.java new file mode 100644 index 000000000..4dbb2b8ed --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/OutputCaptureExtension.java @@ -0,0 +1,112 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * JUnit Jupiter {@code @Extension} to capture {@link System#out System.out} and + * {@link System#err System.err}. Can be registered for an entire test class or for an + * individual test method through {@link ExtendWith @ExtendWith}. This extension provides + * {@linkplain ParameterResolver parameter resolution} for a {@link CapturedOutput} + * instance which can be used to assert that the correct output was written. + *

        + * To use with {@link ExtendWith @ExtendWith}, inject the {@link CapturedOutput} as an + * argument to your test class constructor, test method, or lifecycle methods: + * + *

        + * @ExtendWith(OutputCaptureExtension.class)
        + * class MyTest {
        + *
        + *     @Test
        + *     void test(CapturedOutput output) {
        + *         System.out.println("ok");
        + *         assertThat(output).contains("ok");
        + *         System.err.println("error");
        + *     }
        + *
        + *     @AfterEach
        + *     void after(CapturedOutput output) {
        + *         assertThat(output.getOut()).contains("ok");
        + *         assertThat(output.getErr()).contains("error");
        + *     }
        + *
        + * }
        + * 
        + * + * @author Madhura Bhave + * @author Phillip Webb + * @author Andy Wilkinson + * @author Sam Brannen + * @see CapturedOutput + */ +public class OutputCaptureExtension + implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver { + + OutputCaptureExtension() { + } + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + getOutputCapture(context).push(); + } + + @Override + public void afterAll(ExtensionContext context) throws Exception { + getOutputCapture(context).pop(); + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + getOutputCapture(context).push(); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + getOutputCapture(context).pop(); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return CapturedOutput.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return getOutputCapture(extensionContext); + } + + private OutputCapture getOutputCapture(ExtensionContext context) { + return getStore(context).getOrComputeIfAbsent(OutputCapture.class); + } + + private Store getStore(ExtensionContext context) { + return context.getStore(Namespace.create(getClass())); + } + +} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTest.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTest.java new file mode 100644 index 000000000..15eb8d39c --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; + +/** + * Signals that a method is a template for a test that renders a snippet. The test will be + * executed once for each of the two supported snippet formats (Asciidoctor and Markdown). + *

        + * A rendered snippet test method can inject the following types: + *

          + *
        • {@link OperationBuilder}
        • + *
        • {@link AssertableSnippets}
        • + *
        + * + * @author Andy Wilkinson + */ +@TestTemplate +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(RenderedSnippetTestExtension.class) +public @interface RenderedSnippetTest { + + /** + * The snippet formats to render. + * @return the formats + */ + Format[] format() default { Format.ASCIIDOCTOR, Format.MARKDOWN }; + + enum Format { + + /** + * Asciidoctor snippet format. + */ + ASCIIDOCTOR(TemplateFormats.asciidoctor()), + + /** + * Markdown snippet format. + */ + MARKDOWN(TemplateFormats.markdown()); + + private final TemplateFormat templateFormat; + + Format(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; + } + + TemplateFormat templateFormat() { + return this.templateFormat; + } + + } + +} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTestExtension.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTestExtension.java new file mode 100644 index 000000000..34bb83ef9 --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/RenderedSnippetTestExtension.java @@ -0,0 +1,155 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import java.io.File; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; +import org.junit.platform.commons.util.AnnotationUtils; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.testfixtures.jupiter.RenderedSnippetTest.Format; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * {@link TestTemplateInvocationContextProvider} for + * {@link RenderedSnippetTest @RenderedSnippetTest} and + * {@link SnippetTemplate @SnippetTemplate}. + * + * @author Andy Wilkinson + */ +class RenderedSnippetTestExtension implements TestTemplateInvocationContextProvider { + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + return AnnotationUtils.findAnnotation(context.getRequiredTestMethod(), RenderedSnippetTest.class) + .map((renderedSnippetTest) -> Stream.of(renderedSnippetTest.format()) + .map(Format::templateFormat) + .map(SnippetTestInvocationContext::new) + .map(TestTemplateInvocationContext.class::cast)) + .orElseThrow(); + } + + static class SnippetTestInvocationContext implements TestTemplateInvocationContext { + + private final TemplateFormat templateFormat; + + SnippetTestInvocationContext(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; + } + + @Override + public List getAdditionalExtensions() { + return List.of(new RenderedSnippetTestParameterResolver(this.templateFormat)); + } + + @Override + public String getDisplayName(int invocationIndex) { + return this.templateFormat.getId(); + } + + } + + static class RenderedSnippetTestParameterResolver implements ParameterResolver { + + private final TemplateFormat templateFormat; + + RenderedSnippetTestParameterResolver(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + Class parameterType = parameterContext.getParameter().getType(); + return AssertableSnippets.class.equals(parameterType) || OperationBuilder.class.equals(parameterType) + || TemplateFormat.class.equals(parameterType); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + Class parameterType = parameterContext.getParameter().getType(); + if (AssertableSnippets.class.equals(parameterType)) { + return getStore(extensionContext).getOrComputeIfAbsent(AssertableSnippets.class, + (key) -> new AssertableSnippets(determineOutputDirectory(extensionContext), + determineOperationName(extensionContext), this.templateFormat)); + } + if (TemplateFormat.class.equals(parameterType)) { + return this.templateFormat; + } + return getStore(extensionContext).getOrComputeIfAbsent(OperationBuilder.class, (key) -> { + OperationBuilder operationBuilder = new OperationBuilder(determineOutputDirectory(extensionContext), + determineOperationName(extensionContext), this.templateFormat); + AnnotationUtils.findAnnotation(extensionContext.getRequiredTestMethod(), SnippetTemplate.class) + .ifPresent((snippetTemplate) -> { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource(snippetTemplate.snippet())) + .willReturn(snippetResource(snippetTemplate.template(), this.templateFormat)); + operationBuilder.attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)); + }); + + return operationBuilder; + }); + } + + private Store getStore(ExtensionContext extensionContext) { + return extensionContext.getStore(Namespace.create(getClass())); + } + + private File determineOutputDirectory(ExtensionContext extensionContext) { + return new File("build/" + extensionContext.getRequiredTestClass().getSimpleName()); + } + + private String determineOperationName(ExtensionContext extensionContext) { + String operationName = extensionContext.getRequiredTestMethod().getName(); + int index = operationName.indexOf('['); + if (index > 0) { + operationName = operationName.substring(0, index); + } + return operationName; + } + + private FileSystemResource snippetResource(String name, TemplateFormat templateFormat) { + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + templateFormat.getId() + "/" + name + ".snippet"); + } + + } + +} diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTemplate.java similarity index 53% rename from samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java rename to spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTemplate.java index c336ff6a0..a07e34ce7 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/NullOrNotBlank.java +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,33 +14,33 @@ * limitations under the License. */ -package com.example.notes; +package org.springframework.restdocs.testfixtures.jupiter; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import javax.validation.Constraint; -import javax.validation.Payload; -import javax.validation.constraints.Null; - -import org.hibernate.validator.constraints.CompositionType; -import org.hibernate.validator.constraints.ConstraintComposition; -import org.hibernate.validator.constraints.NotBlank; - -@ConstraintComposition(CompositionType.OR) -@Constraint(validatedBy = {}) -@Null -@NotBlank -@Target({ ElementType.FIELD, ElementType.PARAMETER }) +/** + * Customizes the template that will be used when rendering a snippet in a + * {@link RenderedSnippetTest rendered snippet test}. + * + * @author Andy Wilkinson + */ @Retention(RetentionPolicy.RUNTIME) -public @interface NullOrNotBlank { - - String message() default "Must be null or not blank"; - - Class[] groups() default {}; - - Class[] payload() default {}; +@Target(ElementType.METHOD) +public @interface SnippetTemplate { + + /** + * The name of the snippet whose template should be customized. + * @return the snippet name + */ + String snippet(); + + /** + * The custom template to use when rendering the snippet. + * @return the custom template + */ + String template(); } diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTest.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTest.java new file mode 100644 index 000000000..291f3e6f7 --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Signals that a method is a test of a {@link Snippet}. Typically used to test scenarios + * where a failure occurs before the snippet is rendered. To test snippet rendering, use + * {@link RenderedSnippetTest}. + *

        + * A snippet test method can inject the following types: + *

          + *
        • {@link OperationBuilder}
        • + *
        + * + * @author Andy Wilkinson + */ +@Test +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(SnippetTestExtension.class) +public @interface SnippetTest { + +} diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTestExtension.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTestExtension.java new file mode 100644 index 000000000..1488902fc --- /dev/null +++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/jupiter/SnippetTestExtension.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.testfixtures.jupiter; + +import java.io.File; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * {@link ParameterResolver} for {@link SnippetTest @SnippetTest}. + * + * @author Andy Wilkinson + */ +class SnippetTestExtension implements ParameterResolver { + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + Class parameterType = parameterContext.getParameter().getType(); + return OperationBuilder.class.equals(parameterType); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return getStore(extensionContext).getOrComputeIfAbsent(OperationBuilder.class, + (key) -> new OperationBuilder(determineOutputDirectory(extensionContext), + determineOperationName(extensionContext))); + } + + private Store getStore(ExtensionContext extensionContext) { + return extensionContext.getStore(Namespace.create(getClass())); + } + + private File determineOutputDirectory(ExtensionContext extensionContext) { + return new File("build/" + extensionContext.getRequiredTestClass().getSimpleName()); + } + + private String determineOperationName(ExtensionContext extensionContext) { + String operationName = extensionContext.getRequiredTestMethod().getName(); + int index = operationName.indexOf('['); + if (index > 0) { + operationName = operationName.substring(0, index); + } + return operationName; + } + +} diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index d8a0529cb..875d74148 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -1,18 +1,23 @@ -description = 'Spring REST Docs MockMvc' +plugins { + id "java-library" + id "maven-publish" + id "optional-dependencies" +} + +description = "Spring REST Docs MockMvc" dependencies { - compile 'org.springframework:spring-test' - compile project(':spring-restdocs-core') - optional 'junit:junit' - runtime 'org.springframework:spring-webmvc' + api(project(":spring-restdocs-core")) + api("org.springframework:spring-webmvc") + api("org.springframework:spring-test") + + implementation("jakarta.servlet:jakarta.servlet-api") - testCompile 'org.mockito:mockito-core' - testCompile 'org.hamcrest:hamcrest-library' - testCompile 'org.springframework.hateoas:spring-hateoas' - testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') - testRuntime 'commons-logging:commons-logging:1.2' + internal(platform(project(":spring-restdocs-platform"))) + + testImplementation(testFixtures(project(":spring-restdocs-core"))) } -test { - jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" -} \ No newline at end of file +tasks.named("test") { + useJUnitPlatform() +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java index 628d1a283..ef9ea0e13 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,12 +57,11 @@ public void remove() { /** * Creates an {@code Iterable} that will iterate over the given {@code enumeration}. - * * @param the type of the enumeration's elements - * @param enumeration The enumeration to expose as an {@code Iterable} + * @param enumeration the enumeration to expose as an {@code Iterable} * @return the iterable */ - static Iterable iterable(Enumeration enumeration) { + static Iterable of(Enumeration enumeration) { return new IterableEnumeration<>(enumeration); } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationPreprocessorsConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationPreprocessorsConfigurer.java new file mode 100644 index 000000000..44afd43bc --- /dev/null +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationPreprocessorsConfigurer.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014-2019 the original author or 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. + */ + +package org.springframework.restdocs.mockmvc; + +import org.springframework.restdocs.config.OperationPreprocessorsConfigurer; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.web.context.WebApplicationContext; + +/** + * A configurer that can be used to configure the operation preprocessors. + * + * @author Filip Hrisafov + * @since 2.0.0 + */ +public final class MockMvcOperationPreprocessorsConfigurer extends + OperationPreprocessorsConfigurer + implements MockMvcConfigurer { + + MockMvcOperationPreprocessorsConfigurer(MockMvcRestDocumentationConfigurer parent) { + super(parent); + } + + @Override + public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { + and().afterConfigurerAdded(builder); + } + + @Override + public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder builder, + WebApplicationContext context) { + return and().beforeMockMvcCreated(builder, context); + } + +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index 5c127ddf7..580bc763f 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,15 +17,21 @@ package org.springframework.restdocs.mockmvc; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; import java.net.URI; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Scanner; -import javax.servlet.ServletException; -import javax.servlet.http.Part; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Part; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -37,67 +43,129 @@ import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationRequestPartFactory; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.util.FileCopyUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; -import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; - /** * A converter for creating an {@link OperationRequest} from a * {@link MockHttpServletRequest}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ class MockMvcRequestConverter implements RequestConverter { - private static final String SCHEME_HTTP = "http"; - - private static final String SCHEME_HTTPS = "https"; - - private static final int STANDARD_PORT_HTTP = 80; - - private static final int STANDARD_PORT_HTTPS = 443; - @Override public OperationRequest convert(MockHttpServletRequest mockRequest) { try { HttpHeaders headers = extractHeaders(mockRequest); - Parameters parameters = extractParameters(mockRequest); List parts = extractParts(mockRequest); - String queryString = mockRequest.getQueryString(); - if (!StringUtils.hasText(queryString) - && "GET".equals(mockRequest.getMethod())) { - queryString = parameters.toQueryString(); - } - return new OperationRequestFactory().create( - URI.create( - getRequestUri(mockRequest) + (StringUtils.hasText(queryString) - ? "?" + queryString : "")), - HttpMethod.valueOf(mockRequest.getMethod()), - FileCopyUtils.copyToByteArray(mockRequest.getInputStream()), headers, - parameters, parts); + Collection cookies = extractCookies(mockRequest, headers); + return new OperationRequestFactory().create(getRequestUri(mockRequest), + HttpMethod.valueOf(mockRequest.getMethod()), getRequestContent(mockRequest, headers), headers, + parts, cookies); } catch (Exception ex) { throw new ConversionException(ex); } } + private URI getRequestUri(MockHttpServletRequest mockRequest) { + String queryString = ""; + if (mockRequest.getQueryString() != null) { + queryString = mockRequest.getQueryString(); + } + else if ("GET".equals(mockRequest.getMethod()) || mockRequest.getContentLengthLong() > 0) { + queryString = urlEncodedParameters(mockRequest); + } + StringBuffer requestUrlBuffer = mockRequest.getRequestURL(); + if (queryString.length() > 0) { + requestUrlBuffer.append("?").append(queryString.toString()); + } + return URI.create(requestUrlBuffer.toString()); + } + + private String urlEncodedParameters(MockHttpServletRequest mockRequest) { + StringBuilder parameters = new StringBuilder(); + MultiValueMap queryParameters = parse(mockRequest.getQueryString()); + for (String name : IterableEnumeration.of(mockRequest.getParameterNames())) { + if (!queryParameters.containsKey(name)) { + String[] values = mockRequest.getParameterValues(name); + if (values.length == 0) { + append(parameters, name); + } + else { + for (String value : values) { + append(parameters, name, value); + } + } + } + } + return parameters.toString(); + } + + private byte[] getRequestContent(MockHttpServletRequest mockRequest, HttpHeaders headers) { + byte[] content = mockRequest.getContentAsByteArray(); + if ("GET".equals(mockRequest.getMethod())) { + return content; + } + MediaType contentType = headers.getContentType(); + if (contentType == null || MediaType.APPLICATION_FORM_URLENCODED.includes(contentType)) { + Map parameters = mockRequest.getParameterMap(); + if (!parameters.isEmpty() && (content == null || content.length == 0)) { + StringBuilder contentBuilder = new StringBuilder(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap queryParameters = parse(mockRequest.getQueryString()); + mockRequest.getParameterMap().forEach((name, values) -> { + List queryParameterValues = queryParameters.get(name); + if (values.length == 0) { + if (queryParameterValues == null) { + append(contentBuilder, name); + } + } + else { + for (String value : values) { + if (queryParameterValues == null || !queryParameterValues.contains(value)) { + append(contentBuilder, name, value); + } + } + } + }); + return contentBuilder.toString().getBytes(StandardCharsets.UTF_8); + } + } + return content; + } + + private Collection extractCookies(MockHttpServletRequest mockRequest, HttpHeaders headers) { + if (mockRequest.getCookies() == null || mockRequest.getCookies().length == 0) { + return Collections.emptyList(); + } + List cookies = new ArrayList<>(); + for (jakarta.servlet.http.Cookie servletCookie : mockRequest.getCookies()) { + cookies.add(new RequestCookie(servletCookie.getName(), servletCookie.getValue())); + } + headers.remove(HttpHeaders.COOKIE); + return cookies; + } + private List extractParts(MockHttpServletRequest servletRequest) throws IOException, ServletException { List parts = new ArrayList<>(); parts.addAll(extractServletRequestParts(servletRequest)); if (servletRequest instanceof MockMultipartHttpServletRequest) { - parts.addAll(extractMultipartRequestParts( - (MockMultipartHttpServletRequest) servletRequest)); + parts.addAll(extractMultipartRequestParts((MockMultipartHttpServletRequest) servletRequest)); } return parts; } - private List extractServletRequestParts( - MockHttpServletRequest servletRequest) throws IOException, ServletException { + private List extractServletRequestParts(MockHttpServletRequest servletRequest) + throws IOException, ServletException { List parts = new ArrayList<>(); for (Part part : servletRequest.getParts()) { parts.add(createOperationRequestPart(part)); @@ -105,24 +173,21 @@ private List extractServletRequestParts( return parts; } - private OperationRequestPart createOperationRequestPart(Part part) - throws IOException { + private OperationRequestPart createOperationRequestPart(Part part) throws IOException { HttpHeaders partHeaders = extractHeaders(part); List contentTypeHeader = partHeaders.get(HttpHeaders.CONTENT_TYPE); if (part.getContentType() != null && contentTypeHeader == null) { partHeaders.setContentType(MediaType.parseMediaType(part.getContentType())); } return new OperationRequestPartFactory().create(part.getName(), - StringUtils.hasText(part.getSubmittedFileName()) - ? part.getSubmittedFileName() : null, + StringUtils.hasText(part.getSubmittedFileName()) ? part.getSubmittedFileName() : null, FileCopyUtils.copyToByteArray(part.getInputStream()), partHeaders); } - private List extractMultipartRequestParts( - MockMultipartHttpServletRequest multipartRequest) throws IOException { + private List extractMultipartRequestParts(MockMultipartHttpServletRequest multipartRequest) + throws IOException { List parts = new ArrayList<>(); - for (Entry> entry : multipartRequest.getMultiFileMap() - .entrySet()) { + for (Entry> entry : multipartRequest.getMultiFileMap().entrySet()) { for (MultipartFile file : entry.getValue()) { parts.add(createOperationRequestPart(file)); } @@ -130,16 +195,14 @@ private List extractMultipartRequestParts( return parts; } - private OperationRequestPart createOperationRequestPart(MultipartFile file) - throws IOException { + private OperationRequestPart createOperationRequestPart(MultipartFile file) throws IOException { HttpHeaders partHeaders = new HttpHeaders(); if (StringUtils.hasText(file.getContentType())) { partHeaders.setContentType(MediaType.parseMediaType(file.getContentType())); } return new OperationRequestPartFactory().create(file.getName(), - StringUtils.hasText(file.getOriginalFilename()) - ? file.getOriginalFilename() : null, - file.getBytes(), partHeaders); + StringUtils.hasText(file.getOriginalFilename()) ? file.getOriginalFilename() : null, file.getBytes(), + partHeaders); } private HttpHeaders extractHeaders(Part part) { @@ -152,43 +215,72 @@ private HttpHeaders extractHeaders(Part part) { return partHeaders; } - private Parameters extractParameters(MockHttpServletRequest servletRequest) { - Parameters parameters = new Parameters(); - for (String name : iterable(servletRequest.getParameterNames())) { - for (String value : servletRequest.getParameterValues(name)) { - parameters.add(name, value); - } - } - return parameters; - } - private HttpHeaders extractHeaders(MockHttpServletRequest servletRequest) { HttpHeaders headers = new HttpHeaders(); - for (String headerName : iterable(servletRequest.getHeaderNames())) { - for (String value : iterable(servletRequest.getHeaders(headerName))) { + for (String headerName : IterableEnumeration.of(servletRequest.getHeaderNames())) { + for (String value : IterableEnumeration.of(servletRequest.getHeaders(headerName))) { headers.add(headerName, value); } } return headers; } - private boolean isNonStandardPort(MockHttpServletRequest request) { - return (SCHEME_HTTP.equals(request.getScheme()) - && request.getServerPort() != STANDARD_PORT_HTTP) - || (SCHEME_HTTPS.equals(request.getScheme()) - && request.getServerPort() != STANDARD_PORT_HTTPS); + private static void append(StringBuilder sb, String key) { + append(sb, key, ""); } - private String getRequestUri(MockHttpServletRequest request) { - StringWriter uriWriter = new StringWriter(); - PrintWriter printer = new PrintWriter(uriWriter); + private static void append(StringBuilder sb, String key, String value) { + doAppend(sb, urlEncode(key) + "=" + urlEncode(value)); + } + + private static void doAppend(StringBuilder sb, String toAppend) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(toAppend); + } - printer.printf("%s://%s", request.getScheme(), request.getServerName()); - if (isNonStandardPort(request)) { - printer.printf(":%d", request.getServerPort()); + private static String urlEncode(String s) { + if (!StringUtils.hasLength(s)) { + return ""; } - printer.print(request.getRequestURI()); - return uriWriter.toString(); + return URLEncoder.encode(s, StandardCharsets.UTF_8); + } + + private static MultiValueMap parse(String query) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + if (!StringUtils.hasLength(query)) { + return parameters; + } + try (Scanner scanner = new Scanner(query)) { + scanner.useDelimiter("&"); + while (scanner.hasNext()) { + processParameter(scanner.next(), parameters); + } + } + return parameters; + } + + private static void processParameter(String parameter, MultiValueMap parameters) { + String[] components = parameter.split("="); + if (components.length > 0 && components.length < 3) { + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + List values = parameters.computeIfAbsent(components[0], (p) -> new LinkedList<>()); + values.add(""); + } + } + else { + throw new IllegalArgumentException("The parameter '" + parameter + "' is malformed"); + } + } + + private static String decode(String encoded) { + return URLDecoder.decode(encoded, StandardCharsets.US_ASCII); } } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java index 3633ad61c..701d86998 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,26 +16,48 @@ package org.springframework.restdocs.mockmvc; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import jakarta.servlet.http.Cookie; + import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.operation.ResponseCookie; +import org.springframework.util.StringUtils; /** * A converter for creating an {@link OperationResponse} derived from a * {@link MockHttpServletResponse}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ class MockMvcResponseConverter implements ResponseConverter { @Override public OperationResponse convert(MockHttpServletResponse mockResponse) { - return new OperationResponseFactory().create( - HttpStatus.valueOf(mockResponse.getStatus()), - extractHeaders(mockResponse), mockResponse.getContentAsByteArray()); + HttpHeaders headers = extractHeaders(mockResponse); + Collection cookies = extractCookies(mockResponse); + return new OperationResponseFactory().create(HttpStatusCode.valueOf(mockResponse.getStatus()), headers, + mockResponse.getContentAsByteArray(), cookies); + } + + private Collection extractCookies(MockHttpServletResponse mockResponse) { + if (mockResponse.getCookies() == null || mockResponse.getCookies().length == 0) { + return Collections.emptyList(); + } + List cookies = new ArrayList<>(); + for (Cookie cookie : mockResponse.getCookies()) { + cookies.add(new ResponseCookie(cookie.getName(), cookie.getValue())); + } + return cookies; } private HttpHeaders extractHeaders(MockHttpServletResponse response) { @@ -45,7 +67,56 @@ private HttpHeaders extractHeaders(MockHttpServletResponse response) { headers.add(headerName, value); } } + + if (response.getCookies() != null && !headers.containsHeader(HttpHeaders.SET_COOKIE)) { + for (Cookie cookie : response.getCookies()) { + headers.add(HttpHeaders.SET_COOKIE, generateSetCookieHeader(cookie)); + } + } + return headers; } + private String generateSetCookieHeader(Cookie cookie) { + StringBuilder header = new StringBuilder(); + + header.append(cookie.getName()); + header.append('='); + + appendIfAvailable(header, cookie.getValue()); + + int maxAge = cookie.getMaxAge(); + if (maxAge > -1) { + header.append(";Max-Age="); + header.append(maxAge); + } + + appendIfAvailable(header, "; Domain=", cookie.getDomain()); + appendIfAvailable(header, "; Path=", cookie.getPath()); + + if (cookie.getSecure()) { + header.append("; Secure"); + } + + if (cookie.isHttpOnly()) { + header.append("; HttpOnly"); + } + + return header.toString(); + } + + private void appendIfAvailable(StringBuilder header, String value) { + if (StringUtils.hasText(value)) { + header.append(""); + header.append(value); + } + } + + private void appendIfAvailable(StringBuilder header, String name, String value) { + if (StringUtils.hasText(value)) { + header.append(name); + header.append(value); + } + } + } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index 87d9f93f6..3ff48f524 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,27 +41,9 @@ private MockMvcRestDocumentation() { } - /** - * Provides access to a {@link MockMvcConfigurer} that can be used to configure a - * {@link MockMvc} instance using the given {@code restDocumentation}. - * - * @param restDocumentation the REST documentation - * @return the configurer - * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) - * @deprecated Since 1.1 in favor of - * {@link #documentationConfiguration(RestDocumentationContextProvider)} - */ - @Deprecated - public static MockMvcRestDocumentationConfigurer documentationConfiguration( - org.springframework.restdocs.RestDocumentation restDocumentation) { - return documentationConfiguration( - (RestDocumentationContextProvider) restDocumentation); - } - /** * Provides access to a {@link MockMvcConfigurer} that can be used to configure a * {@link MockMvc} instance using the given {@code contextProvider}. - * * @param contextProvider the context provider * @return the configurer * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) @@ -74,24 +56,21 @@ public static MockMvcRestDocumentationConfigurer documentationConfiguration( /** * Documents the API call with the given {@code identifier} using the given * {@code snippets} in addition to any default snippets. - * * @param identifier an identifier for the API call that is being documented * @param snippets the snippets * @return a Mock MVC {@code ResultHandler} that will produce the documentation * @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder) * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) */ - public static RestDocumentationResultHandler document(String identifier, - Snippet... snippets) { - return new RestDocumentationResultHandler(new RestDocumentationGenerator<>( - identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); + public static RestDocumentationResultHandler document(String identifier, Snippet... snippets) { + return new RestDocumentationResultHandler( + new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); } /** * Documents the API call with the given {@code identifier} using the given * {@code snippets} in addition to any default snippets. The given * {@code requestPreprocessor} is applied to the request before it is documented. - * * @param identifier an identifier for the API call that is being documented * @param requestPreprocessor the request preprocessor * @param snippets the snippets @@ -101,16 +80,14 @@ public static RestDocumentationResultHandler document(String identifier, */ public static RestDocumentationResultHandler document(String identifier, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler( - new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, - RESPONSE_CONVERTER, requestPreprocessor, snippets)); + return new RestDocumentationResultHandler(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, + RESPONSE_CONVERTER, requestPreprocessor, snippets)); } /** * Documents the API call with the given {@code identifier} using the given * {@code snippets} in addition to any default snippets. The given * {@code responsePreprocessor} is applied to the request before it is documented. - * * @param identifier an identifier for the API call that is being documented * @param responsePreprocessor the response preprocessor * @param snippets the snippets @@ -120,9 +97,8 @@ public static RestDocumentationResultHandler document(String identifier, */ public static RestDocumentationResultHandler document(String identifier, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler( - new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, - RESPONSE_CONVERTER, responsePreprocessor, snippets)); + return new RestDocumentationResultHandler(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, + RESPONSE_CONVERTER, responsePreprocessor, snippets)); } /** @@ -130,7 +106,6 @@ public static RestDocumentationResultHandler document(String identifier, * {@code snippets} in addition to any default snippets. The given * {@code requestPreprocessor} and {@code responsePreprocessor} are applied to the * request and response respectively before they are documented. - * * @param identifier an identifier for the API call that is being documented * @param requestPreprocessor the request preprocessor * @param responsePreprocessor the response preprocessor @@ -140,11 +115,10 @@ public static RestDocumentationResultHandler document(String identifier, * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) */ public static RestDocumentationResultHandler document(String identifier, - OperationRequestPreprocessor requestPreprocessor, - OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(new RestDocumentationGenerator<>( - identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, - responsePreprocessor, snippets)); + OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor, + Snippet... snippets) { + return new RestDocumentationResultHandler(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, + RESPONSE_CONVERTER, requestPreprocessor, responsePreprocessor, snippets)); } } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index f7d983a9d..9c495ebb3 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,10 @@ package org.springframework.restdocs.mockmvc; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; +import java.util.function.Function; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentationContext; @@ -27,23 +29,27 @@ import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.util.ReflectionUtils; import org.springframework.web.context.WebApplicationContext; /** * A MockMvc-specific {@link RestDocumentationConfigurer}. * * @author Andy Wilkinson + * @author Filip Hrisafov * @since 1.1.0 */ public final class MockMvcRestDocumentationConfigurer extends - RestDocumentationConfigurer + RestDocumentationConfigurer implements MockMvcConfigurer { - private final MockMvcSnippetConfigurer snippetConfigurer = new MockMvcSnippetConfigurer( - this); + private final MockMvcSnippetConfigurer snippetConfigurer = new MockMvcSnippetConfigurer(this); private final UriConfigurer uriConfigurer = new UriConfigurer(this); + private final MockMvcOperationPreprocessorsConfigurer operationPreprocessorsConfigurer = new MockMvcOperationPreprocessorsConfigurer( + this); + private final RestDocumentationContextProvider contextManager; MockMvcRestDocumentationConfigurer(RestDocumentationContextProvider contextManager) { @@ -53,7 +59,6 @@ public final class MockMvcRestDocumentationConfigurer extends /** * Returns a {@link UriConfigurer} that can be used to configure the request URIs that * will be documented. - * * @return the URI configurer */ public UriConfigurer uris() { @@ -61,8 +66,8 @@ public UriConfigurer uris() { } @Override - public RequestPostProcessor beforeMockMvcCreated( - ConfigurableMockMvcBuilder builder, WebApplicationContext context) { + public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder builder, + WebApplicationContext context) { return new ConfigurerApplyingRequestPostProcessor(this.contextManager); } @@ -76,13 +81,36 @@ public MockMvcSnippetConfigurer snippets() { return this.snippetConfigurer; } - private final class ConfigurerApplyingRequestPostProcessor - implements RequestPostProcessor { + @Override + public MockMvcOperationPreprocessorsConfigurer operationPreprocessors() { + return this.operationPreprocessorsConfigurer; + } + + private final class ConfigurerApplyingRequestPostProcessor implements RequestPostProcessor { + + private static final Function urlTemplateExtractor; + + static { + Function fromRequestAttribute = ( + request) -> (String) request.getAttribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE); + Function extractor; + try { + Method accessorMethod = MockHttpServletRequest.class.getMethod("getUriTemplate"); + extractor = (request) -> { + String urlTemplate = fromRequestAttribute.apply(request); + return (urlTemplate != null) ? urlTemplate + : (String) ReflectionUtils.invokeMethod(accessorMethod, request); + }; + } + catch (Exception ex) { + extractor = fromRequestAttribute; + } + urlTemplateExtractor = extractor; + } private final RestDocumentationContextProvider contextManager; - private ConfigurerApplyingRequestPostProcessor( - RestDocumentationContextProvider contextManager) { + private ConfigurerApplyingRequestPostProcessor(RestDocumentationContextProvider contextManager) { this.contextManager = contextManager; } @@ -92,17 +120,14 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) Map configuration = new HashMap<>(); configuration.put(MockHttpServletRequest.class.getName(), request); configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - request.getAttribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE)); + urlTemplateExtractor.apply(request)); configuration.put(RestDocumentationContext.class.getName(), context); - request.setAttribute( - RestDocumentationResultHandler.ATTRIBUTE_NAME_CONFIGURATION, - configuration); + request.setAttribute(RestDocumentationResultHandler.ATTRIBUTE_NAME_CONFIGURATION, configuration); MockMvcRestDocumentationConfigurer.this.apply(configuration, context); - MockMvcRestDocumentationConfigurer.this.uriConfigurer.apply(configuration, - context); + MockMvcRestDocumentationConfigurer.this.uriConfigurer.apply(configuration, context); return request; } + } } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java index df35126d4..ef6bb5908 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,8 +29,7 @@ * @since 1.1.0 */ public final class MockMvcSnippetConfigurer extends - SnippetConfigurer - implements MockMvcConfigurer { + SnippetConfigurer implements MockMvcConfigurer { MockMvcSnippetConfigurer(MockMvcRestDocumentationConfigurer parent) { super(parent); @@ -42,8 +41,9 @@ public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { } @Override - public RequestPostProcessor beforeMockMvcCreated( - ConfigurableMockMvcBuilder builder, WebApplicationContext context) { + public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder builder, + WebApplicationContext context) { return and().beforeMockMvcCreated(builder, context); } + } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java index b4f84c750..30f302041 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,20 +46,17 @@ private RestDocumentationRequestBuilders() { /** * Create a {@link MockHttpServletRequestBuilder} for a GET request. The url template * will be captured and made available for documentation. - * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the GET request */ - public static MockHttpServletRequestBuilder get(String urlTemplate, - Object... urlVariables) { - return MockMvcRequestBuilders.get(urlTemplate, urlVariables).requestAttr( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + public static MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables) { + return MockMvcRequestBuilders.get(urlTemplate, urlVariables) + .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** * Create a {@link MockHttpServletRequestBuilder} for a GET request. - * * @param uri the URL * @return the builder for the GET request */ @@ -70,20 +67,17 @@ public static MockHttpServletRequestBuilder get(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for a POST request. The url template * will be captured and made available for documentation. - * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the POST request */ - public static MockHttpServletRequestBuilder post(String urlTemplate, - Object... urlVariables) { - return MockMvcRequestBuilders.post(urlTemplate, urlVariables).requestAttr( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + public static MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables) { + return MockMvcRequestBuilders.post(urlTemplate, urlVariables) + .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** * Create a {@link MockHttpServletRequestBuilder} for a POST request. - * * @param uri the URL * @return the builder for the POST request */ @@ -94,20 +88,17 @@ public static MockHttpServletRequestBuilder post(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for a PUT request. The url template * will be captured and made available for documentation. - * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the PUT request */ - public static MockHttpServletRequestBuilder put(String urlTemplate, - Object... urlVariables) { - return MockMvcRequestBuilders.put(urlTemplate, urlVariables).requestAttr( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + public static MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables) { + return MockMvcRequestBuilders.put(urlTemplate, urlVariables) + .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** * Create a {@link MockHttpServletRequestBuilder} for a PUT request. - * * @param uri the URL * @return the builder for the PUT request */ @@ -118,20 +109,17 @@ public static MockHttpServletRequestBuilder put(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for a PATCH request. The url * template will be captured and made available for documentation. - * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the PATCH request */ - public static MockHttpServletRequestBuilder patch(String urlTemplate, - Object... urlVariables) { - return MockMvcRequestBuilders.patch(urlTemplate, urlVariables).requestAttr( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + public static MockHttpServletRequestBuilder patch(String urlTemplate, Object... urlVariables) { + return MockMvcRequestBuilders.patch(urlTemplate, urlVariables) + .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** * Create a {@link MockHttpServletRequestBuilder} for a PATCH request. - * * @param uri the URL * @return the builder for the PATCH request */ @@ -142,20 +130,17 @@ public static MockHttpServletRequestBuilder patch(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for a DELETE request. The url * template will be captured and made available for documentation. - * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the DELETE request */ - public static MockHttpServletRequestBuilder delete(String urlTemplate, - Object... urlVariables) { - return MockMvcRequestBuilders.delete(urlTemplate, urlVariables).requestAttr( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + public static MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables) { + return MockMvcRequestBuilders.delete(urlTemplate, urlVariables) + .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** * Create a {@link MockHttpServletRequestBuilder} for a DELETE request. - * * @param uri the URL * @return the builder for the DELETE request */ @@ -166,20 +151,17 @@ public static MockHttpServletRequestBuilder delete(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for an OPTIONS request. The url * template will be captured and made available for documentation. - * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the OPTIONS request */ - public static MockHttpServletRequestBuilder options(String urlTemplate, - Object... urlVariables) { - return MockMvcRequestBuilders.options(urlTemplate, urlVariables).requestAttr( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + public static MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables) { + return MockMvcRequestBuilders.options(urlTemplate, urlVariables) + .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** * Create a {@link MockHttpServletRequestBuilder} for an OPTIONS request. - * * @param uri the URL * @return the builder for the OPTIONS request */ @@ -190,20 +172,17 @@ public static MockHttpServletRequestBuilder options(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for a HEAD request. The url template * will be captured and made available for documentation. - * * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the HEAD request */ - public static MockHttpServletRequestBuilder head(String urlTemplate, - Object... urlVariables) { - return MockMvcRequestBuilders.head(urlTemplate, urlVariables).requestAttr( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + public static MockHttpServletRequestBuilder head(String urlTemplate, Object... urlVariables) { + return MockMvcRequestBuilders.head(urlTemplate, urlVariables) + .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** * Create a {@link MockHttpServletRequestBuilder} for a HEAD request. - * * @param uri the URL * @return the builder for the HEAD request */ @@ -214,17 +193,15 @@ public static MockHttpServletRequestBuilder head(URI uri) { /** * Create a {@link MockHttpServletRequestBuilder} for a request with the given HTTP * method. The url template will be captured and made available for documentation. - * * @param httpMethod the HTTP method * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables * @return the builder for the request */ - public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, - String urlTemplate, Object... urlVariables) { + public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, + Object... urlVariables) { return MockMvcRequestBuilders.request(httpMethod, urlTemplate, urlVariables) - .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - urlTemplate); + .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -239,29 +216,26 @@ public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, URI u } /** - * Create a {@link MockHttpServletRequestBuilder} for a multipart request. The url - * template will be captured and made available for documentation. - * + * Create a {@link MockMultipartHttpServletRequestBuilder} for a multipart request. + * The URL template will be captured and made available for documentation. * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables - * @return the builder for the file upload request + * @return the builder for the multipart request + * @since 2.0.6 */ - public static MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, - Object... urlVariables) { - return (MockMultipartHttpServletRequestBuilder) MockMvcRequestBuilders - .fileUpload(urlTemplate, urlVariables) - .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - urlTemplate); + public static MockMultipartHttpServletRequestBuilder multipart(String urlTemplate, Object... urlVariables) { + return (MockMultipartHttpServletRequestBuilder) MockMvcRequestBuilders.multipart(urlTemplate, urlVariables) + .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** - * Create a {@link MockHttpServletRequestBuilder} for a multipart request. - * + * Create a {@link MockMultipartHttpServletRequestBuilder} for a multipart request. * @param uri the URL - * @return the builder for the file upload request + * @return the builder for the multipart request + * @since 2.0.6 */ - public static MockMultipartHttpServletRequestBuilder fileUpload(URI uri) { - return MockMvcRequestBuilders.fileUpload(uri); + public static MockMultipartHttpServletRequestBuilder multipart(URI uri) { + return MockMvcRequestBuilders.multipart(uri); } } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 5afc7d3b0..33b2f3d85 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,26 +48,8 @@ public class RestDocumentationResultHandler implements ResultHandler { } @Override - public void handle(MvcResult result) throws Exception { - @SuppressWarnings("unchecked") - Map configuration = (Map) result.getRequest() - .getAttribute(ATTRIBUTE_NAME_CONFIGURATION); - this.delegate.handle(result.getRequest(), result.getResponse(), configuration); - } - - /** - * Adds the given {@code snippets} such that they are documented when this result - * handler is called. - * - * @param snippets the snippets to add - * @return this {@code RestDocumentationResultHandler} - * @deprecated since 1.1 in favor of {@link #document(Snippet...)} and passing the - * return value into {@link ResultActions#andDo(ResultHandler)} - */ - @Deprecated - public RestDocumentationResultHandler snippets(Snippet... snippets) { - this.delegate.addSnippets(snippets); - return this; + public void handle(MvcResult result) { + this.delegate.handle(result.getRequest(), result.getResponse(), retrieveConfiguration(result)); } /** @@ -82,7 +64,6 @@ public RestDocumentationResultHandler snippets(Snippet... snippets) { * fieldWithPath("page").description("The requested Page") * )); * - * * @param snippets the snippets * @return the new result handler */ @@ -90,25 +71,30 @@ public RestDocumentationResultHandler document(Snippet... snippets) { return new RestDocumentationResultHandler(this.delegate.withSnippets(snippets)) { @Override - public void handle(MvcResult result) throws Exception { - @SuppressWarnings("unchecked") - Map configuration = new HashMap<>( - (Map) result.getRequest() - .getAttribute(ATTRIBUTE_NAME_CONFIGURATION)); - configuration.remove( - RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); - getDelegate().handle(result.getRequest(), result.getResponse(), - configuration); + public void handle(MvcResult result) { + Map configuration = new HashMap<>(retrieveConfiguration(result)); + configuration.remove(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + getDelegate().handle(result.getRequest(), result.getResponse(), configuration); } + }; } /** * Returns the {@link RestDocumentationGenerator} that is used as a delegate. - * * @return the delegate */ protected final RestDocumentationGenerator getDelegate() { return this.delegate; } + + private Map retrieveConfiguration(MvcResult result) { + @SuppressWarnings("unchecked") + Map configuration = (Map) result.getRequest() + .getAttribute(ATTRIBUTE_NAME_CONFIGURATION); + Assert.state(configuration != null, () -> "REST Docs configuration not found. Did you forget to apply a " + + MockMvcRestDocumentationConfigurer.class.getSimpleName() + " when building the MockMvc instance?"); + return configuration; + } + } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java index 277308bec..1e53d1dd5 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,8 +31,7 @@ * * @author Andy Wilkinson */ -public class UriConfigurer - extends AbstractNestedConfigurer +public class UriConfigurer extends AbstractNestedConfigurer implements MockMvcConfigurer { /** @@ -69,8 +68,7 @@ public class UriConfigurer /** * Configures any documented URIs to use the given {@code scheme}. The default is * {@code http}. - * - * @param scheme The URI scheme + * @param scheme the URI scheme * @return {@code this} */ public UriConfigurer withScheme(String scheme) { @@ -81,8 +79,7 @@ public UriConfigurer withScheme(String scheme) { /** * Configures any documented URIs to use the given {@code host}. The default is * {@code localhost}. - * - * @param host The URI host + * @param host the URI host * @return {@code this} */ public UriConfigurer withHost(String host) { @@ -93,8 +90,7 @@ public UriConfigurer withHost(String host) { /** * Configures any documented URIs to use the given {@code port}. The default is * {@code 8080}. - * - * @param port The URI port + * @param port the URI port * @return {@code this} */ public UriConfigurer withPort(int port) { @@ -103,10 +99,9 @@ public UriConfigurer withPort(int port) { } @Override - public void apply(Map configuration, - RestDocumentationContext context) { + public void apply(Map configuration, RestDocumentationContext context) { MockHttpServletRequest request = (MockHttpServletRequest) configuration - .get(MockHttpServletRequest.class.getName()); + .get(MockHttpServletRequest.class.getName()); request.setScheme(this.scheme); request.setServerPort(this.port); request.setServerName(this.host); @@ -118,8 +113,8 @@ public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { } @Override - public RequestPostProcessor beforeMockMvcCreated( - ConfigurableMockMvcBuilder builder, WebApplicationContext context) { + public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder builder, + WebApplicationContext context) { return and().beforeMockMvcCreated(builder, context); } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java index a05bfd1e9..c0d3c1fc3 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,13 @@ package org.springframework.restdocs.mockmvc; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.net.URI; import java.util.Arrays; +import java.util.Iterator; -import javax.servlet.http.Part; - -import org.junit.Test; +import jakarta.servlet.http.Part; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -31,15 +32,12 @@ import org.springframework.mock.web.MockServletContext; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.hasEntry; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -48,187 +46,194 @@ * * @author Andy Wilkinson */ -public class MockMvcRequestConverterTests { +class MockMvcRequestConverterTests { private final MockMvcRequestConverter factory = new MockMvcRequestConverter(); @Test - public void httpRequest() throws Exception { - OperationRequest request = createOperationRequest( - MockMvcRequestBuilders.get("/foo")); - assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); - assertThat(request.getMethod(), is(HttpMethod.GET)); + void httpRequest() { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders.get("/foo")); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); } @Test - public void httpRequestWithCustomPort() throws Exception { - MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") - .buildRequest(new MockServletContext()); + void httpRequestWithCustomPort() { + MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo").buildRequest(new MockServletContext()); mockRequest.setServerPort(8080); OperationRequest request = this.factory.convert(mockRequest); - assertThat(request.getUri(), is(URI.create("http://localhost:8080/foo"))); - assertThat(request.getMethod(), is(HttpMethod.GET)); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:8080/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); + } + + @Test + void requestWithContextPath() { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders.get("/foo/bar").contextPath("/foo")); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo/bar")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); } @Test - public void requestWithContextPath() throws Exception { + void requestWithHeaders() { OperationRequest request = createOperationRequest( - MockMvcRequestBuilders.get("/foo/bar").contextPath("/foo")); - assertThat(request.getUri(), is(URI.create("http://localhost/foo/bar"))); - assertThat(request.getMethod(), is(HttpMethod.GET)); + MockMvcRequestBuilders.get("/foo").header("a", "alpha", "apple").header("b", "bravo")); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); + assertThat(request.getHeaders().headerSet()).contains(entry("a", Arrays.asList("alpha", "apple")), + entry("b", Arrays.asList("bravo"))); } @Test - public void requestWithHeaders() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .get("/foo").header("a", "alpha", "apple").header("b", "bravo")); - assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); - assertThat(request.getMethod(), is(HttpMethod.GET)); - assertThat(request.getHeaders(), hasEntry("a", Arrays.asList("alpha", "apple"))); - assertThat(request.getHeaders(), hasEntry("b", Arrays.asList("bravo"))); + void requestWithCookies() { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders.get("/foo") + .cookie(new jakarta.servlet.http.Cookie("cookieName1", "cookieVal1"), + new jakarta.servlet.http.Cookie("cookieName2", "cookieVal2"))); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); + assertThat(request.getCookies().size()).isEqualTo(2); + + Iterator cookieIterator = request.getCookies().iterator(); + + RequestCookie cookie1 = cookieIterator.next(); + assertThat(cookie1.getName()).isEqualTo("cookieName1"); + assertThat(cookie1.getValue()).isEqualTo("cookieVal1"); + + RequestCookie cookie2 = cookieIterator.next(); + assertThat(cookie2.getName()).isEqualTo("cookieName2"); + assertThat(cookie2.getValue()).isEqualTo("cookieVal2"); } @Test - public void httpsRequest() throws Exception { - MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") - .buildRequest(new MockServletContext()); + void httpsRequest() { + MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo").buildRequest(new MockServletContext()); mockRequest.setScheme("https"); mockRequest.setServerPort(443); OperationRequest request = this.factory.convert(mockRequest); - assertThat(request.getUri(), is(URI.create("https://localhost/foo"))); - assertThat(request.getMethod(), is(HttpMethod.GET)); + assertThat(request.getUri()).isEqualTo(URI.create("https://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); } @Test - public void httpsRequestWithCustomPort() throws Exception { - MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") - .buildRequest(new MockServletContext()); + void httpsRequestWithCustomPort() { + MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo").buildRequest(new MockServletContext()); mockRequest.setScheme("https"); mockRequest.setServerPort(8443); OperationRequest request = this.factory.convert(mockRequest); - assertThat(request.getUri(), is(URI.create("https://localhost:8443/foo"))); - assertThat(request.getMethod(), is(HttpMethod.GET)); + assertThat(request.getUri()).isEqualTo(URI.create("https://localhost:8443/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); + } + + @Test + void getRequestWithParametersProducesUriWithQueryString() { + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.get("/foo").param("a", "alpha", "apple").param("b", "br&vo")); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?a=alpha&a=apple&b=br%26vo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); } @Test - public void getRequestWithParametersProducesUriWithQueryString() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .get("/foo").param("a", "alpha", "apple").param("b", "br&vo")); - assertThat(request.getUri(), - is(URI.create("http://localhost/foo?a=alpha&a=apple&b=br%26vo"))); - assertThat(request.getParameters().size(), is(2)); - assertThat(request.getParameters(), - hasEntry("a", Arrays.asList("alpha", "apple"))); - assertThat(request.getParameters(), hasEntry("b", Arrays.asList("br&vo"))); - assertThat(request.getMethod(), is(HttpMethod.GET)); + void getRequestWithQueryString() { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/foo?a=alpha&b=bravo"); + OperationRequest request = createOperationRequest(builder); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?a=alpha&b=bravo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); } @Test - public void getRequestWithQueryStringPopulatesParameters() throws Exception { + void postRequestWithParametersCreatesFormUrlEncodedContent() { OperationRequest request = createOperationRequest( - MockMvcRequestBuilders.get("/foo?a=alpha&b=bravo")); - assertThat(request.getUri(), - is(URI.create("http://localhost/foo?a=alpha&b=bravo"))); - assertThat(request.getParameters().size(), is(2)); - assertThat(request.getParameters(), hasEntry("a", Arrays.asList("alpha"))); - assertThat(request.getParameters(), hasEntry("b", Arrays.asList("bravo"))); - assertThat(request.getMethod(), is(HttpMethod.GET)); + MockMvcRequestBuilders.post("/foo").param("a", "alpha", "apple").param("b", "br&vo")); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(request.getContentAsString()).isEqualTo("a=alpha&a=apple&b=br%26vo"); + assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED); } @Test - public void postRequestWithParameters() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .post("/foo").param("a", "alpha", "apple").param("b", "br&vo")); - assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); - assertThat(request.getMethod(), is(HttpMethod.POST)); - assertThat(request.getParameters().size(), is(2)); - assertThat(request.getParameters(), - hasEntry("a", Arrays.asList("alpha", "apple"))); - assertThat(request.getParameters(), hasEntry("b", Arrays.asList("br&vo"))); + void postRequestWithParametersAndQueryStringCreatesFormUrlEncodedContentWithoutDuplication() { + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.post("/foo?a=alpha").param("a", "apple").param("b", "br&vo")); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?a=alpha")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(request.getContentAsString()).isEqualTo("a=apple&b=br%26vo"); + assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED); } @Test - public void mockMultipartFileUpload() throws Exception { - OperationRequest request = createOperationRequest( - MockMvcRequestBuilders.fileUpload("/foo") - .file(new MockMultipartFile("file", new byte[] { 1, 2, 3, 4 }))); - assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); - assertThat(request.getMethod(), is(HttpMethod.POST)); - assertThat(request.getParts().size(), is(1)); + void mockMultipartFileUpload() { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders.multipart("/foo") + .file(new MockMultipartFile("file", new byte[] { 1, 2, 3, 4 }))); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(request.getParts().size()).isEqualTo(1); OperationRequestPart part = request.getParts().iterator().next(); - assertThat(part.getName(), is(equalTo("file"))); - assertThat(part.getSubmittedFileName(), is(nullValue())); - assertThat(part.getHeaders().size(), is(1)); - assertThat(part.getHeaders().getContentLength(), is(4L)); - assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); + assertThat(part.getName()).isEqualTo("file"); + assertThat(part.getSubmittedFileName()).isNull(); + assertThat(part.getHeaders().size()).isEqualTo(1); + assertThat(part.getHeaders().getContentLength()).isEqualTo(4L); + assertThat(part.getContent()).isEqualTo(new byte[] { 1, 2, 3, 4 }); } @Test - public void mockMultipartFileUploadWithContentType() throws Exception { - OperationRequest request = createOperationRequest( - MockMvcRequestBuilders.fileUpload("/foo").file(new MockMultipartFile( - "file", "original", "image/png", new byte[] { 1, 2, 3, 4 }))); - assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); - assertThat(request.getMethod(), is(HttpMethod.POST)); - assertThat(request.getParts().size(), is(1)); + void mockMultipartFileUploadWithContentType() { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders.multipart("/foo") + .file(new MockMultipartFile("file", "original", "image/png", new byte[] { 1, 2, 3, 4 }))); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(request.getParts().size()).isEqualTo(1); OperationRequestPart part = request.getParts().iterator().next(); - assertThat(part.getName(), is(equalTo("file"))); - assertThat(part.getSubmittedFileName(), is(equalTo("original"))); - assertThat(part.getHeaders().getContentType(), is(MediaType.IMAGE_PNG)); - assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); + assertThat(part.getName()).isEqualTo("file"); + assertThat(part.getSubmittedFileName()).isEqualTo("original"); + assertThat(part.getHeaders().getContentType()).isEqualTo(MediaType.IMAGE_PNG); + assertThat(part.getContent()).isEqualTo(new byte[] { 1, 2, 3, 4 }); } @Test - public void requestWithPart() throws Exception { - MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") - .buildRequest(new MockServletContext()); + void requestWithPart() throws IOException { + MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo").buildRequest(new MockServletContext()); Part mockPart = mock(Part.class); given(mockPart.getHeaderNames()).willReturn(Arrays.asList("a", "b")); given(mockPart.getHeaders("a")).willReturn(Arrays.asList("alpha")); given(mockPart.getHeaders("b")).willReturn(Arrays.asList("bravo", "banana")); - given(mockPart.getInputStream()) - .willReturn(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); + given(mockPart.getInputStream()).willReturn(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); given(mockPart.getName()).willReturn("part-name"); given(mockPart.getSubmittedFileName()).willReturn("submitted.txt"); mockRequest.addPart(mockPart); OperationRequest request = this.factory.convert(mockRequest); - assertThat(request.getParts().size(), is(1)); + assertThat(request.getParts().size()).isEqualTo(1); OperationRequestPart part = request.getParts().iterator().next(); - assertThat(part.getName(), is(equalTo("part-name"))); - assertThat(part.getSubmittedFileName(), is(equalTo("submitted.txt"))); - assertThat(part.getHeaders().getContentType(), is(nullValue())); - assertThat(part.getHeaders().get("a"), contains("alpha")); - assertThat(part.getHeaders().get("b"), contains("bravo", "banana")); - assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); + assertThat(part.getName()).isEqualTo("part-name"); + assertThat(part.getSubmittedFileName()).isEqualTo("submitted.txt"); + assertThat(part.getHeaders().getContentType()).isNull(); + assertThat(part.getHeaders().get("a")).containsExactly("alpha"); + assertThat(part.getHeaders().get("b")).containsExactly("bravo", "banana"); + assertThat(part.getContent()).isEqualTo(new byte[] { 1, 2, 3, 4 }); } @Test - public void requestWithPartWithContentType() throws Exception { - MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") - .buildRequest(new MockServletContext()); + void requestWithPartWithContentType() throws IOException { + MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo").buildRequest(new MockServletContext()); Part mockPart = mock(Part.class); given(mockPart.getHeaderNames()).willReturn(Arrays.asList("a", "b")); given(mockPart.getHeaders("a")).willReturn(Arrays.asList("alpha")); given(mockPart.getHeaders("b")).willReturn(Arrays.asList("bravo", "banana")); - given(mockPart.getInputStream()) - .willReturn(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); + given(mockPart.getInputStream()).willReturn(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); given(mockPart.getName()).willReturn("part-name"); given(mockPart.getSubmittedFileName()).willReturn("submitted.png"); given(mockPart.getContentType()).willReturn("image/png"); mockRequest.addPart(mockPart); OperationRequest request = this.factory.convert(mockRequest); - assertThat(request.getParts().size(), is(1)); + assertThat(request.getParts().size()).isEqualTo(1); OperationRequestPart part = request.getParts().iterator().next(); - assertThat(part.getName(), is(equalTo("part-name"))); - assertThat(part.getSubmittedFileName(), is(equalTo("submitted.png"))); - assertThat(part.getHeaders().getContentType(), is(MediaType.IMAGE_PNG)); - assertThat(part.getHeaders().get("a"), contains("alpha")); - assertThat(part.getHeaders().get("b"), contains("bravo", "banana")); - assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); + assertThat(part.getName()).isEqualTo("part-name"); + assertThat(part.getSubmittedFileName()).isEqualTo("submitted.png"); + assertThat(part.getHeaders().getContentType()).isEqualTo(MediaType.IMAGE_PNG); + assertThat(part.getHeaders().get("a")).containsExactly("alpha"); + assertThat(part.getHeaders().get("b")).containsExactly("bravo", "banana"); + assertThat(part.getContent()).isEqualTo(new byte[] { 1, 2, 3, 4 }); } - private OperationRequest createOperationRequest(MockHttpServletRequestBuilder builder) - throws Exception { + private OperationRequest createOperationRequest(MockHttpServletRequestBuilder builder) { return this.factory.convert(builder.buildRequest(new MockServletContext())); } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java new file mode 100644 index 000000000..e13bb03be --- /dev/null +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.mockmvc; + +import java.util.Collections; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.ResponseCookie; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link MockMvcResponseConverter}. + * + * @author Tomasz Kopczynski + */ +class MockMvcResponseConverterTests { + + private final MockMvcResponseConverter factory = new MockMvcResponseConverter(); + + @Test + void basicResponse() { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setStatus(HttpServletResponse.SC_OK); + OperationResponse operationResponse = this.factory.convert(response); + assertThat(operationResponse.getStatus()).isEqualTo(HttpStatus.OK); + } + + @Test + void responseWithCookie() { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setStatus(HttpServletResponse.SC_OK); + Cookie cookie = new Cookie("name", "value"); + cookie.setDomain("localhost"); + cookie.setHttpOnly(true); + response.addCookie(cookie); + OperationResponse operationResponse = this.factory.convert(response); + assertThat(operationResponse.getHeaders().headerSet()).containsOnly( + entry(HttpHeaders.SET_COOKIE, Collections.singletonList("name=value; Domain=localhost; HttpOnly"))); + assertThat(operationResponse.getCookies()).hasSize(1); + assertThat(operationResponse.getCookies()).first().extracting(ResponseCookie::getName).isEqualTo("name"); + assertThat(operationResponse.getCookies()).first().extracting(ResponseCookie::getValue).isEqualTo("value"); + } + + @Test + void responseWithCustomStatus() { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setStatus(600); + OperationResponse operationResponse = this.factory.convert(response); + assertThat(operationResponse.getStatus()).isEqualTo(HttpStatusCode.valueOf(600)); + } + +} diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java index d03e9de62..dfb2debc9 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,25 @@ package org.springframework.restdocs.mockmvc; -import java.net.URI; +import java.lang.reflect.Method; +import java.util.Map; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.hateoas.mvc.BasicLinkBuilder; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.util.ReflectionUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponents; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link MockMvcRestDocumentationConfigurer}. @@ -39,72 +42,92 @@ * @author Andy Wilkinson * @author Dmitriy Mayboroda */ -public class MockMvcRestDocumentationConfigurerTests { +@ExtendWith(RestDocumentationExtension.class) +class MockMvcRestDocumentationConfigurerTests { private MockHttpServletRequest request = new MockHttpServletRequest(); - @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("test"); - @Test - public void defaultConfiguration() { - RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( - this.restDocumentation).beforeMockMvcCreated(null, null); + void defaultConfiguration(RestDocumentationContextProvider restDocumentation) { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation) + .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); - assertUriConfiguration("http", "localhost", 8080); } @Test - public void customScheme() { - RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( - this.restDocumentation).uris().withScheme("https") - .beforeMockMvcCreated(null, null); + void customScheme(RestDocumentationContextProvider restDocumentation) { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation).uris() + .withScheme("https") + .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); - assertUriConfiguration("https", "localhost", 8080); } @Test - public void customHost() { - RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( - this.restDocumentation).uris().withHost("api.example.com") - .beforeMockMvcCreated(null, null); + void customHost(RestDocumentationContextProvider restDocumentation) { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation).uris() + .withHost("api.example.com") + .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); - assertUriConfiguration("http", "api.example.com", 8080); } @Test - public void customPort() { - RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( - this.restDocumentation).uris().withPort(8081).beforeMockMvcCreated(null, - null); + void customPort(RestDocumentationContextProvider restDocumentation) { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation).uris() + .withPort(8081) + .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); - assertUriConfiguration("http", "localhost", 8081); } @Test - public void noContentLengthHeaderWhenRequestHasNotContent() { - RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( - this.restDocumentation).uris().withPort(8081).beforeMockMvcCreated(null, - null); + void noContentLengthHeaderWhenRequestHasNotContent(RestDocumentationContextProvider restDocumentation) { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation).uris() + .withPort(8081) + .beforeMockMvcCreated(null, null); + postProcessor.postProcessRequest(this.request); + assertThat(this.request.getHeader("Content-Length")).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + void uriTemplateFromRequestAttribute(RestDocumentationContextProvider restDocumentation) { + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation) + .beforeMockMvcCreated(null, null); + this.request.setAttribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "{a}/{b}"); + postProcessor.postProcessRequest(this.request); + Map configuration = (Map) this.request + .getAttribute(RestDocumentationResultHandler.ATTRIBUTE_NAME_CONFIGURATION); + assertThat(configuration).containsEntry(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "{a}/{b}"); + } + + @Test + @SuppressWarnings("unchecked") + void uriTemplateFromRequest(RestDocumentationContextProvider restDocumentation) { + Method setUriTemplate = ReflectionUtils.findMethod(MockHttpServletRequest.class, "setUriTemplate", + String.class); + Assumptions.assumeFalse(setUriTemplate == null); + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer(restDocumentation) + .beforeMockMvcCreated(null, null); + ReflectionUtils.invokeMethod(setUriTemplate, this.request, "{a}/{b}"); postProcessor.postProcessRequest(this.request); - assertThat(this.request.getHeader("Content-Length"), is(nullValue())); + Map configuration = (Map) this.request + .getAttribute(RestDocumentationResultHandler.ATTRIBUTE_NAME_CONFIGURATION); + assertThat(configuration).containsEntry(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "{a}/{b}"); } private void assertUriConfiguration(String scheme, String host, int port) { - assertEquals(scheme, this.request.getScheme()); - assertEquals(host, this.request.getServerName()); - assertEquals(port, this.request.getServerPort()); - RequestContextHolder - .setRequestAttributes(new ServletRequestAttributes(this.request)); + assertThat(scheme).isEqualTo(this.request.getScheme()); + assertThat(host).isEqualTo(this.request.getServerName()); + assertThat(port).isEqualTo(this.request.getServerPort()); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(this.request)); try { - URI uri = BasicLinkBuilder.linkToCurrentMapping().toUri(); - assertEquals(scheme, uri.getScheme()); - assertEquals(host, uri.getHost()); - assertEquals(port, uri.getPort()); + UriComponents uriComponents = ServletUriComponentsBuilder.fromCurrentServletMapping().build(); + assertThat(scheme).isEqualTo(uriComponents.getScheme()); + assertThat(host).isEqualTo(uriComponents.getHost()); + assertThat(port).isEqualTo(uriComponents.getPort()); } finally { RequestContextHolder.resetRequestAttributes(); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 4258c41c4..49460f1e3 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,19 +17,27 @@ package org.springframework.restdocs.mockmvc; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; import java.net.URL; import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Pattern; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -38,15 +46,22 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationIntegrationTests.TestConfiguration; -import org.springframework.restdocs.test.SnippetMatchers.HttpRequestMatcher; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.testfixtures.SnippetConditions; +import org.springframework.restdocs.testfixtures.SnippetConditions.CodeBlockCondition; +import org.springframework.restdocs.testfixtures.SnippetConditions.HttpRequestCondition; +import org.springframework.restdocs.testfixtures.SnippetConditions.HttpResponseCondition; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.FileCopyUtils; import org.springframework.util.FileSystemUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -54,42 +69,32 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.operation.preprocess.Preprocessors.maskLinks; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.replacePattern; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; -import static org.springframework.restdocs.templates.TemplateFormats.markdown; -import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; -import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; -import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; -import static org.springframework.restdocs.test.SnippetMatchers.snippet; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -98,454 +103,574 @@ * * @author Andy Wilkinson * @author Dewet Diener + * @author Tomasz Kopczynski + * @author Filip Hrisafov */ -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig @WebAppConfiguration +@ExtendWith(RestDocumentationExtension.class) @ContextConfiguration(classes = TestConfiguration.class) public class MockMvcRestDocumentationIntegrationTests { - @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build/generated-snippets"); + private RestDocumentationContextProvider restDocumentation; @Autowired private WebApplicationContext context; - @Before - public void deleteSnippets() { + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + this.restDocumentation = restDocumentation; FileSystemUtils.deleteRecursively(new File("build/generated-snippets")); } - @After - public void clearOutputDirSystemProperty() { + @AfterEach + void clearOutputDirSystemProperty() { System.clearProperty("org.springframework.restdocs.outputDir"); } @Test - public void basicSnippetGeneration() throws Exception { + void basicSnippetGeneration() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation) - .snippets().withEncoding("UTF-8")) - .build(); - + .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation).snippets().withEncoding("UTF-8")) + .build(); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andDo(document("basic")); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/basic"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc"); + .andExpect(status().isOk()) + .andDo(document("basic")); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/basic"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); } @Test - public void markdownSnippetGeneration() throws Exception { + void getRequestWithBody() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation) - .snippets().withEncoding("UTF-8").withTemplateFormat(markdown())) - .build(); + .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation).snippets().withEncoding("UTF-8")) + .build(); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON).content("some body content")) + .andExpect(status().isOk()) + .andDo(document("get-request-with-body")); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/get-request-with-body"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + } + @Test + void markdownSnippetGeneration() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation).snippets() + .withEncoding("UTF-8") + .withTemplateFormat(TemplateFormats.markdown())) + .build(); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andDo(document("basic-markdown")); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/basic-markdown"), "http-request.md", + .andExpect(status().isOk()) + .andDo(document("basic-markdown")); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/basic-markdown"), "http-request.md", "http-response.md", "curl-request.md"); } @Test - public void curlSnippetWithContent() throws Exception { + void curlSnippetWithContent() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - + .apply(documentationConfiguration(this.restDocumentation)) + .build(); mockMvc.perform(post("/").accept(MediaType.APPLICATION_JSON).content("content")) - .andExpect(status().isOk()).andDo(document("curl-snippet-with-content")); - assertThat( - new File( - "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), - is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") - .content("$ curl " + "'http://localhost:8080/' -i -X POST " - + "-H 'Accept: application/json' -d 'content'")))); + .andExpect(status().isOk()) + .andDo(document("curl-snippet-with-content")); + assertThat(new File("build/generated-snippets/curl-snippet-with-content/curl-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ curl 'http://localhost:8080/' -i -X POST \\%n" + + " -H 'Accept: application/json' \\%n" + " -d 'content'")))); } @Test - public void curlSnippetWithQueryStringOnPost() throws Exception { + void curlSnippetWithCookies() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(post("/?foo=bar").param("foo", "bar").param("a", "alpha") - .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) - .andDo(document("curl-snippet-with-query-string")); - assertThat( - new File( - "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), - is(snippet(asciidoctor()) - .withContents(codeBlock(asciidoctor(), "bash").content("$ curl " - + "'http://localhost:8080/?foo=bar' -i -X POST " - + "-H 'Accept: application/json' -d 'a=alpha'")))); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON).cookie(new Cookie("cookieName", "cookieVal"))) + .andExpect(status().isOk()) + .andDo(document("curl-snippet-with-cookies")); + assertThat(new File("build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ curl 'http://localhost:8080/' -i -X GET \\%n" + + " -H 'Accept: application/json' \\%n" + " --cookie 'cookieName=cookieVal'")))); } @Test - public void curlSnippetWithContentAndParametersOnPost() throws Exception { + void curlSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(post("/").param("a", "alpha").accept(MediaType.APPLICATION_JSON) - .content("some content")).andExpect(status().isOk()) - .andDo(document("curl-snippet-with-content-and-parameters")); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + mockMvc.perform(post("/?foo=bar").param("a", "alpha").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("curl-snippet-with-query-string")); + assertThat(new File("build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ curl " + "'http://localhost:8080/?foo=bar' -i -X POST \\%n" + + " -H 'Accept: application/json' \\%n" + " -d 'a=alpha'")))); + } + + @Test + void curlSnippetWithEmptyParameterQueryString() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + mockMvc.perform(get("/").param("a", "").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("curl-snippet-with-empty-parameter-query-string")); assertThat( - new File( - "build/generated-snippets/curl-snippet-with-content-and-parameters/curl-request.adoc"), - is(snippet(asciidoctor()) - .withContents(codeBlock(asciidoctor(), "bash").content("$ curl " - + "'http://localhost:8080/?a=alpha' -i -X POST " - + "-H 'Accept: application/json' -d 'some content'")))); + new File("build/generated-snippets/curl-snippet-with-empty-parameter-query-string/curl-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash").withContent(String + .format("$ curl 'http://localhost:8080/?a=' -i -X GET \\%n" + " -H 'Accept: application/json'")))); } @Test - public void httpieSnippetWithContent() throws Exception { + void curlSnippetWithContentAndParametersOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + mockMvc.perform(post("/").param("a", "alpha").accept(MediaType.APPLICATION_JSON).content("some content")) + .andExpect(status().isOk()) + .andDo(document("curl-snippet-with-content-and-parameters")); + assertThat(new File("build/generated-snippets/curl-snippet-with-content-and-parameters/curl-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ curl 'http://localhost:8080/?a=alpha' -i -X POST \\%n" + + " -H 'Accept: application/json' \\%n" + " -d 'some content'")))); + } + @Test + void httpieSnippetWithContent() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) + .build(); mockMvc.perform(post("/").accept(MediaType.APPLICATION_JSON).content("content")) - .andExpect(status().isOk()) - .andDo(document("httpie-snippet-with-content")); - assertThat( - new File( - "build/generated-snippets/httpie-snippet-with-content/httpie-request.adoc"), - is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") - .content("$ echo 'content' | http POST 'http://localhost:8080/'" - + " 'Accept:application/json'")))); + .andExpect(status().isOk()) + .andDo(document("httpie-snippet-with-content")); + assertThat(new File("build/generated-snippets/httpie-snippet-with-content/httpie-request.adoc")).has( + content(codeBlock(TemplateFormats.asciidoctor(), "bash").withContent(String.format("$ echo 'content' | " + + "http POST 'http://localhost:8080/' \\%n" + " 'Accept:application/json'")))); } @Test - public void httpieSnippetWithQueryStringOnPost() throws Exception { + void httpieSnippetWithCookies() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(post("/?foo=bar").param("foo", "bar").param("a", "alpha") - .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) - .andDo(document("httpie-snippet-with-query-string")); - assertThat( - new File( - "build/generated-snippets/httpie-snippet-with-query-string/httpie-request.adoc"), - is(snippet(asciidoctor()) - .withContents(codeBlock(asciidoctor(), "bash").content("$ http " - + "--form POST 'http://localhost:8080/?foo=bar' " - + "'Accept:application/json' 'a=alpha'")))); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON).cookie(new Cookie("cookieName", "cookieVal"))) + .andExpect(status().isOk()) + .andDo(document("httpie-snippet-with-cookies")); + assertThat(new File("build/generated-snippets/httpie-snippet-with-cookies/httpie-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ http GET 'http://localhost:8080/' \\%n" + + " 'Accept:application/json' \\%n" + " 'Cookie:cookieName=cookieVal'")))); } @Test - public void httpieSnippetWithContentAndParametersOnPost() throws Exception { + void httpieSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(post("/").param("a", "alpha").content("some content") - .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) - .andDo(document("httpie-snippet-post-with-content-and-parameters")); - assertThat( - new File( - "build/generated-snippets/httpie-snippet-post-with-content-and-parameters/httpie-request.adoc"), - is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") - .content("$ echo " + "'some content' | http POST " - + "'http://localhost:8080/?a=alpha' " - + "'Accept:application/json'")))); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + mockMvc.perform(post("/?foo=bar").param("a", "alpha").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("httpie-snippet-with-query-string")); + assertThat(new File("build/generated-snippets/httpie-snippet-with-query-string/httpie-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ http --form POST 'http://localhost:8080/?foo=bar' \\%n" + + " 'Accept:application/json' \\%n 'a=alpha'")))); } @Test - public void linksSnippet() throws Exception { + void httpieSnippetWithContentAndParametersOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + mockMvc.perform(post("/").param("a", "alpha").content("some content").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("httpie-snippet-post-with-content-and-parameters")); + assertThat(new File( + "build/generated-snippets/httpie-snippet-post-with-content-and-parameters/httpie-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ echo " + "'some content' | http POST " + + "'http://localhost:8080/?a=alpha' \\%n" + " 'Accept:application/json'")))); + } + @Test + void linksSnippet() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) + .build(); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andDo(document("links", - links(linkWithRel("rel").description("The description")))); + .andExpect(status().isOk()) + .andDo(document("links", links(linkWithRel("rel").description("The description")))); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc", - "links.adoc"); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "links.adoc"); } @Test - public void pathParametersSnippet() throws Exception { + void pathParametersSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - - mockMvc.perform(get("{foo}", "/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andDo(document("links", pathParameters( - parameterWithName("foo").description("The description")))); - - assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc", - "path-parameters.adoc"); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + mockMvc.perform(get("/{foo}", "").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("links", pathParameters(parameterWithName("foo").description("The description")))); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "path-parameters.adoc"); } @Test - public void requestParametersSnippet() throws Exception { + void queryParametersSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - + .apply(documentationConfiguration(this.restDocumentation)) + .build(); mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andDo(document("links", requestParameters( - parameterWithName("foo").description("The description")))); - - assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc", - "request-parameters.adoc"); + .andExpect(status().isOk()) + .andDo(document("links", queryParameters(parameterWithName("foo").description("The description")))); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "query-parameters.adoc"); } @Test - public void requestFieldsSnippet() throws Exception { + void requestFieldsSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - - mockMvc.perform(get("/").param("foo", "bar").content("{\"a\":\"alpha\"}") - .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) - .andDo(document("links", requestFields( - fieldWithPath("a").description("The description")))); - - assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc", - "request-fields.adoc"); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + mockMvc.perform(get("/").param("foo", "bar").content("{\"a\":\"alpha\"}").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("links", requestFields(fieldWithPath("a").description("The description")))); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "request-fields.adoc"); } @Test - public void requestPartsSnippet() throws Exception { + void requestPartsSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - - mockMvc.perform(fileUpload("/upload").file("foo", "bar".getBytes())) - .andExpect(status().isOk()).andDo(document("request-parts", requestParts( - partWithName("foo").description("The description")))); - - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/request-parts"), "http-request.adoc", + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + mockMvc.perform(multipart("/upload").file("foo", "bar".getBytes())) + .andExpect(status().isOk()) + .andDo(document("request-parts", requestParts(partWithName("foo").description("The description")))); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/request-parts"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", "request-parts.adoc"); } @Test - public void responseFieldsSnippet() throws Exception { + void responseFieldsSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - + .apply(documentationConfiguration(this.restDocumentation)) + .build(); mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("links", - responseFields(fieldWithPath("a") - .description("The description"), - fieldWithPath("links").description("Links to other resources")))); - - assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc", - "response-fields.adoc"); + .andExpect(status().isOk()) + .andDo(document("links", responseFields(fieldWithPath("a").description("The description"), + subsectionWithPath("links").description("Links to other resources")))); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "response-fields.adoc"); } @Test - public void parameterizedOutputDirectory() throws Exception { + void responseWithSetCookie() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + mockMvc.perform(get("/set-cookie")) + .andExpect(status().isOk()) + .andDo(document("set-cookie", + responseHeaders(headerWithName(HttpHeaders.SET_COOKIE).description("set-cookie")))); + assertThat(new File("build/generated-snippets/set-cookie/http-response.adoc")) + .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK).header(HttpHeaders.SET_COOKIE, + "name=value; Domain=localhost; HttpOnly"))); + } + @Test + void parameterizedOutputDirectory() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) + .build(); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andDo(document("{method-name}")); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/parameterized-output-directory"), + .andExpect(status().isOk()) + .andDo(document("{method-name}")); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/parameterized-output-directory"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } @Test - public void multiStep() throws Exception { + void multiStep() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(document("{method-name}-{step}")).build(); - - mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-1/"), "http-request.adoc", + .apply(documentationConfiguration(this.restDocumentation)) + .alwaysDo(document("{method-name}-{step}")) + .build(); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/multi-step-1/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); - - mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-2/"), "http-request.adoc", + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/multi-step-2/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); - mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-3/"), "http-request.adoc", + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/multi-step-3/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } @Test - public void alwaysDoWithAdditionalSnippets() throws Exception { + void alwaysDoWithAdditionalSnippets() throws Exception { RestDocumentationResultHandler documentation = document("{method-name}-{step}"); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(documentation).build(); - + .apply(documentationConfiguration(this.restDocumentation)) + .alwaysDo(documentation) + .build(); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andDo(documentation.document( - responseHeaders(headerWithName("a").description("one")))); - - assertExpectedSnippetFilesExist( - new File( - "build/generated-snippets/always-do-with-additional-snippets-1/"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc", - "response-headers.adoc"); + .andExpect(status().isOk()) + .andDo(documentation.document(responseHeaders(headerWithName("a").description("one")))); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/always-do-with-additional-snippets-1/"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", "response-headers.adoc"); } @Test - public void preprocessedRequest() throws Exception { + void preprocessedRequest() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - + .apply(documentationConfiguration(this.restDocumentation)) + .build(); Pattern pattern = Pattern.compile("(\"alpha\")"); - MvcResult result = mockMvc - .perform(get("/").header("a", "alpha").header("b", "bravo") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON).content("{\"a\":\"alpha\"}")) - .andExpect(status().isOk()).andDo(document("original-request")) - .andDo(document("preprocessed-request", - preprocessRequest(prettyPrint(), - removeHeaders("a", HttpHeaders.HOST, - HttpHeaders.CONTENT_LENGTH), - replacePattern(pattern, "\"<>\"")))) - .andReturn(); - - HttpRequestMatcher originalRequest = httpRequest(asciidoctor(), RequestMethod.GET, - "/"); - for (String headerName : iterable(result.getRequest().getHeaderNames())) { + .perform(get("/").header("a", "alpha") + .header("b", "bravo") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content("{\"a\":\"alpha\"}")) + .andExpect(status().isOk()) + .andDo(document("original-request")) + .andDo(document("preprocessed-request", + preprocessRequest(prettyPrint(), + modifyHeaders().remove("a").remove(HttpHeaders.HOST).remove(HttpHeaders.CONTENT_LENGTH), + replacePattern(pattern, "\"<>\"")))) + .andReturn(); + HttpRequestCondition originalRequest = httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/"); + Set mvcResultHeaderNames = new HashSet<>(); + for (String headerName : IterableEnumeration.of(result.getRequest().getHeaderNames())) { originalRequest.header(headerName, result.getRequest().getHeader(headerName)); + mvcResultHeaderNames.add(headerName); } - assertThat( - new File("build/generated-snippets/original-request/http-request.adoc"), - is(snippet(asciidoctor()).withContents(originalRequest - .header("Host", "localhost:8080").header("Content-Length", "13") - .content("{\"a\":\"alpha\"}")))); - HttpRequestMatcher preprocessedRequest = httpRequest(asciidoctor(), - RequestMethod.GET, "/"); - List removedHeaders = Arrays.asList("a", HttpHeaders.HOST, - HttpHeaders.CONTENT_LENGTH); - for (String headerName : iterable(result.getRequest().getHeaderNames())) { + originalRequest.header("Host", "localhost:8080"); + if (!mvcResultHeaderNames.contains("Content-Length")) { + originalRequest.header("Content-Length", "13"); + } + assertThat(new File("build/generated-snippets/original-request/http-request.adoc")) + .has(content(originalRequest.content("{\"a\":\"alpha\"}"))); + HttpRequestCondition preprocessedRequest = httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/"); + List removedHeaders = Arrays.asList("a", HttpHeaders.HOST, HttpHeaders.CONTENT_LENGTH); + for (String headerName : IterableEnumeration.of(result.getRequest().getHeaderNames())) { if (!removedHeaders.contains(headerName)) { - preprocessedRequest.header(headerName, - result.getRequest().getHeader(headerName)); + preprocessedRequest.header(headerName, result.getRequest().getHeader(headerName)); } } String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); - assertThat( - new File( - "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet(asciidoctor()) - .withContents(preprocessedRequest.content(prettyPrinted)))); + assertThat(new File("build/generated-snippets/preprocessed-request/http-request.adoc")) + .has(content(preprocessedRequest.content(prettyPrinted))); + } + + @Test + void defaultPreprocessedRequest() throws Exception { + Pattern pattern = Pattern.compile("(\"alpha\")"); + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .withRequestDefaults(prettyPrint(), + modifyHeaders().remove("a").remove(HttpHeaders.HOST).remove(HttpHeaders.CONTENT_LENGTH), + replacePattern(pattern, "\"<>\""))) + .build(); + + MvcResult result = mockMvc + .perform(get("/").header("a", "alpha") + .header("b", "bravo") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content("{\"a\":\"alpha\"}")) + .andDo(document("default-preprocessed-request")) + .andReturn(); + + HttpRequestCondition preprocessedRequest = httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/"); + List removedHeaders = Arrays.asList("a", HttpHeaders.HOST, HttpHeaders.CONTENT_LENGTH); + for (String headerName : IterableEnumeration.of(result.getRequest().getHeaderNames())) { + if (!removedHeaders.contains(headerName)) { + preprocessedRequest.header(headerName, result.getRequest().getHeader(headerName)); + } + } + String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); + assertThat(new File("build/generated-snippets/default-preprocessed-request/http-request.adoc")) + .has(content(preprocessedRequest.content(prettyPrinted))); } @Test - public void preprocessedResponse() throws Exception { + void preprocessedResponse() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); Pattern pattern = Pattern.compile("(\"alpha\")"); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andDo(document("original-response")) - .andDo(document("preprocessed-response", - preprocessResponse(prettyPrint(), maskLinks(), removeHeaders("a"), - replacePattern(pattern, "\"<>\"")))); - - String original = "{\"a\":\"alpha\",\"links\":[{\"rel\":\"rel\"," - + "\"href\":\"href\"}]}"; - assertThat( - new File("build/generated-snippets/original-response/http-response.adoc"), - is(snippet(asciidoctor()).withContents( - httpResponse(asciidoctor(), HttpStatus.OK).header("a", "alpha") - .header("Content-Type", "application/json;charset=UTF-8") - .header(HttpHeaders.CONTENT_LENGTH, - original.getBytes().length) - .content(original)))); + .andExpect(status().isOk()) + .andDo(document("original-response")) + .andDo(document("preprocessed-response", preprocessResponse(prettyPrint(), maskLinks(), + modifyHeaders().remove("a"), replacePattern(pattern, "\"<>\"")))); + + String original = "{\"a\":\"alpha\",\"links\":[{\"rel\":\"rel\"," + "\"href\":\"href\"}]}"; + assertThat(new File("build/generated-snippets/original-response/http-response.adoc")) + .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK).header("a", "alpha") + .header("Content-Type", "application/json;charset=UTF-8") + .header(HttpHeaders.CONTENT_LENGTH, original.getBytes().length) + .content(original))); String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); - assertThat( - new File( - "build/generated-snippets/preprocessed-response/http-response.adoc"), - is(snippet(asciidoctor()) - .withContents(httpResponse(asciidoctor(), HttpStatus.OK) - .header("Content-Type", "application/json;charset=UTF-8") - .header(HttpHeaders.CONTENT_LENGTH, - prettyPrinted.getBytes().length) - .content(prettyPrinted)))); + assertThat(new File("build/generated-snippets/preprocessed-response/http-response.adoc")) + .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) + .header("Content-Type", "application/json;charset=UTF-8") + .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length) + .content(prettyPrinted))); } @Test - public void customSnippetTemplate() throws Exception { + void defaultPreprocessedResponse() throws Exception { + Pattern pattern = Pattern.compile("(\"alpha\")"); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); + .apply(documentationConfiguration(this.restDocumentation).operationPreprocessors() + .withResponseDefaults(prettyPrint(), maskLinks(), modifyHeaders().remove("a"), + replacePattern(pattern, "\"<>\""))) + .build(); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("default-preprocessed-response")); - ClassLoader classLoader = new URLClassLoader(new URL[] { - new File("src/test/resources/custom-snippet-templates").toURI().toURL() }, + String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); + assertThat(new File("build/generated-snippets/default-preprocessed-response/http-response.adoc")) + .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) + .header("Content-Type", "application/json;charset=UTF-8") + .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length) + .content(prettyPrinted))); + } + + @Test + void customSnippetTemplate() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + ClassLoader classLoader = new URLClassLoader( + new URL[] { new File("src/test/resources/custom-snippet-templates").toURI().toURL() }, getClass().getClassLoader()); ClassLoader previous = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(classLoader); try { mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("custom-snippet-template")); + .andExpect(status().isOk()) + .andDo(document("custom-snippet-template")); } finally { Thread.currentThread().setContextClassLoader(previous); } - assertThat( - new File( - "build/generated-snippets/custom-snippet-template/curl-request.adoc"), - is(snippet(asciidoctor()).withContents(equalTo("Custom curl request")))); - - mockMvc.perform(get("/")).andDo(document("index", curlRequest( - attributes(key("title").value("Access the index using curl"))))); + assertThat(new File("build/generated-snippets/custom-snippet-template/curl-request.adoc")) + .hasContent("Custom curl request"); } @Test - public void customContextPath() throws Exception { + void customContextPath() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + + mockMvc.perform(get("/custom/").contextPath("/custom").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("custom-context-path")); + assertThat(new File("build/generated-snippets/custom-context-path/curl-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash").withContent(String.format( + "$ curl 'http://localhost:8080/custom/' -i -X GET \\%n" + " -H 'Accept: application/json'")))); + } - mockMvc.perform( - get("/custom/").contextPath("/custom").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andDo(document("custom-context-path")); - assertThat( - new File( - "build/generated-snippets/custom-context-path/curl-request.adoc"), - is(snippet(asciidoctor()) - .withContents(codeBlock(asciidoctor(), "bash").content( - "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); + @Test + void exceptionShouldBeThrownWhenCallDocumentMockMvcNotConfigured() { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); + assertThatThrownBy(() -> mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andDo(document("basic"))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("REST Docs configuration not found. Did you " + + "forget to apply a MockMvcRestDocumentationConfigurer when building the MockMvc instance?"); + + } + + @Test + void exceptionShouldBeThrownWhenCallDocumentSnippetsMockMvcNotConfigured() { + RestDocumentationResultHandler documentation = document("{method-name}-{step}"); + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); + assertThatThrownBy(() -> mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andDo(documentation.document(responseHeaders(headerWithName("a").description("one"))))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("REST Docs configuration not found. Did you forget to apply a " + + "MockMvcRestDocumentationConfigurer when building the MockMvc instance?"); } @Test - public void multiPart() throws Exception { + void multiPart() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(fileUpload("/upload").file("test", "content".getBytes())) - .andExpect(status().isOk()).andDo(document("upload", - requestParts(partWithName("test").description("Foo")))); + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + mockMvc.perform(multipart("/upload").file("test", "content".getBytes())) + .andExpect(status().isOk()) + .andDo(document("upload", requestParts(partWithName("test").description("Foo")))); } private void assertExpectedSnippetFilesExist(File directory, String... snippets) { for (String snippet : snippets) { - assertTrue(new File(directory, snippet).isFile()); + assertThat(new File(directory, snippet)).isFile(); } } + private Condition content(final Condition delegate) { + return new Condition<>() { + + @Override + public boolean matches(File value) { + try { + return delegate.matches(FileCopyUtils + .copyToString(new InputStreamReader(new FileInputStream(value), StandardCharsets.UTF_8))); + } + catch (IOException ex) { + fail("Failed to read '" + value + "'", ex); + return false; + } + } + + }; + } + + private CodeBlockCondition codeBlock(TemplateFormat format, String language) { + return SnippetConditions.codeBlock(format, language); + } + + private HttpRequestCondition httpRequest(TemplateFormat format, RequestMethod requestMethod, String uri) { + return SnippetConditions.httpRequest(format, requestMethod, uri); + } + + private HttpResponseCondition httpResponse(TemplateFormat format, HttpStatus status) { + return SnippetConditions.httpResponse(format, status); + } + /** * Test configuration that enables Spring MVC. */ - @Configuration @EnableWebMvc - static class TestConfiguration { + @Configuration(proxyBeanMethods = false) + static final class TestConfiguration { @Bean - public TestController testController() { + TestController testController() { return new TestController(); } } @RestController - private static class TestController { + private static final class TestController { @RequestMapping(value = "/", produces = "application/json;charset=UTF-8") - public ResponseEntity> foo() { + ResponseEntity> foo() { Map response = new HashMap<>(); response.put("a", "alpha"); Map link = new HashMap<>(); @@ -558,13 +683,22 @@ public ResponseEntity> foo() { } @RequestMapping(value = "/company/5", produces = MediaType.APPLICATION_JSON_VALUE) - public String bar() { + String bar() { return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}"; } @RequestMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public void upload() { + void upload() { + + } + + @RequestMapping("/set-cookie") + void setCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("name", "value"); + cookie.setDomain("localhost"); + cookie.setHttpOnly(true); + response.addCookie(cookie); } } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java index d41b36e2b..f689778a6 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,8 @@ import java.net.URI; -import javax.servlet.ServletContext; - -import org.junit.Test; +import jakarta.servlet.ServletContext; +import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; @@ -28,13 +27,11 @@ import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.fileUpload; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.head; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.options; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; @@ -47,114 +44,112 @@ * @author Andy Wilkinson * */ -public class RestDocumentationRequestBuildersTests { +class RestDocumentationRequestBuildersTests { private final ServletContext servletContext = new MockServletContext(); @Test - public void getTemplate() { - assertTemplate(get("{template}", "t"), HttpMethod.GET); + void getTemplate() { + assertTemplate(get("/{template}", "t"), HttpMethod.GET); } @Test - public void getUri() { + void getUri() { assertUri(get(URI.create("/uri")), HttpMethod.GET); } @Test - public void postTemplate() { - assertTemplate(post("{template}", "t"), HttpMethod.POST); + void postTemplate() { + assertTemplate(post("/{template}", "t"), HttpMethod.POST); } @Test - public void postUri() { + void postUri() { assertUri(post(URI.create("/uri")), HttpMethod.POST); } @Test - public void putTemplate() { - assertTemplate(put("{template}", "t"), HttpMethod.PUT); + void putTemplate() { + assertTemplate(put("/{template}", "t"), HttpMethod.PUT); } @Test - public void putUri() { + void putUri() { assertUri(put(URI.create("/uri")), HttpMethod.PUT); } @Test - public void patchTemplate() { - assertTemplate(patch("{template}", "t"), HttpMethod.PATCH); + void patchTemplate() { + assertTemplate(patch("/{template}", "t"), HttpMethod.PATCH); } @Test - public void patchUri() { + void patchUri() { assertUri(patch(URI.create("/uri")), HttpMethod.PATCH); } @Test - public void deleteTemplate() { - assertTemplate(delete("{template}", "t"), HttpMethod.DELETE); + void deleteTemplate() { + assertTemplate(delete("/{template}", "t"), HttpMethod.DELETE); } @Test - public void deleteUri() { + void deleteUri() { assertUri(delete(URI.create("/uri")), HttpMethod.DELETE); } @Test - public void optionsTemplate() { - assertTemplate(options("{template}", "t"), HttpMethod.OPTIONS); + void optionsTemplate() { + assertTemplate(options("/{template}", "t"), HttpMethod.OPTIONS); } @Test - public void optionsUri() { + void optionsUri() { assertUri(options(URI.create("/uri")), HttpMethod.OPTIONS); } @Test - public void headTemplate() { - assertTemplate(head("{template}", "t"), HttpMethod.HEAD); + void headTemplate() { + assertTemplate(head("/{template}", "t"), HttpMethod.HEAD); } @Test - public void headUri() { + void headUri() { assertUri(head(URI.create("/uri")), HttpMethod.HEAD); } @Test - public void requestTemplate() { - assertTemplate(request(HttpMethod.GET, "{template}", "t"), HttpMethod.GET); + void requestTemplate() { + assertTemplate(request(HttpMethod.GET, "/{template}", "t"), HttpMethod.GET); } @Test - public void requestUri() { + void requestUri() { assertUri(request(HttpMethod.GET, URI.create("/uri")), HttpMethod.GET); } @Test - public void fileUploadTemplate() { - assertTemplate(fileUpload("{template}", "t"), HttpMethod.POST); + void multipartTemplate() { + assertTemplate(multipart("/{template}", "t"), HttpMethod.POST); } @Test - public void fileUploadUri() { - assertUri(fileUpload(URI.create("/uri")), HttpMethod.POST); + void multipartUri() { + assertUri(multipart(URI.create("/uri")), HttpMethod.POST); } - private void assertTemplate(MockHttpServletRequestBuilder builder, - HttpMethod httpMethod) { + private void assertTemplate(MockHttpServletRequestBuilder builder, HttpMethod httpMethod) { MockHttpServletRequest request = builder.buildRequest(this.servletContext); - assertThat( - (String) request.getAttribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE), - is(equalTo("{template}"))); - assertThat(request.getRequestURI(), is(equalTo("t"))); - assertThat(request.getMethod(), is(equalTo(httpMethod.name()))); + assertThat((String) request.getAttribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE)) + .isEqualTo("/{template}"); + assertThat(request.getRequestURI()).isEqualTo("/t"); + assertThat(request.getMethod()).isEqualTo(httpMethod.name()); } private void assertUri(MockHttpServletRequestBuilder builder, HttpMethod httpMethod) { MockHttpServletRequest request = builder.buildRequest(this.servletContext); - assertThat(request.getRequestURI(), is(equalTo("/uri"))); - assertThat(request.getMethod(), is(equalTo(httpMethod.name()))); + assertThat(request.getRequestURI()).isEqualTo("/uri"); + assertThat(request.getMethod()).isEqualTo(httpMethod.name()); } + } diff --git a/spring-restdocs-platform/build.gradle b/spring-restdocs-platform/build.gradle new file mode 100644 index 000000000..5e0404db0 --- /dev/null +++ b/spring-restdocs-platform/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'java-platform' +} + +javaPlatform { + allowDependencies() +} + +dependencies { + constraints { + api("com.samskivert:jmustache:$jmustacheVersion") + api("jakarta.servlet:jakarta.servlet-api:6.1.0") + api("jakarta.validation:jakarta.validation-api:3.1.0") + api("org.apache.pdfbox:pdfbox:2.0.27") + api("org.apache.tomcat.embed:tomcat-embed-core:11.0.2") + api("org.apache.tomcat.embed:tomcat-embed-el:11.0.2") + api("org.apiguardian:apiguardian-api:1.1.2") + api("org.asciidoctor:asciidoctorj:3.0.0") + api("org.asciidoctor:asciidoctorj-pdf:2.3.19") + api("org.assertj:assertj-core:3.23.1") + api("org.hamcrest:hamcrest-core:1.3") + api("org.hamcrest:hamcrest-library:1.3") + api("org.hibernate.validator:hibernate-validator:9.0.0.CR1") + api("org.javamoney:moneta:1.4.2") + } + api(enforcedPlatform("com.fasterxml.jackson:jackson-bom:2.14.0")) + api(enforcedPlatform("io.rest-assured:rest-assured-bom:5.2.1")) + api(enforcedPlatform("org.mockito:mockito-bom:4.9.0")) + api(enforcedPlatform("org.junit:junit-bom:5.13.0")) + api(enforcedPlatform("org.springframework:spring-framework-bom:$springFrameworkVersion")) +} diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index 245a697ba..42a05a522 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -1,28 +1,31 @@ -description = 'Spring REST Docs REST Assured' - -boolean isPlatformUsingBootOneFour() { - def bootVersion = dependencyManagement.springIoTestRuntime.managedVersions['org.springframework.boot:spring-boot'] - bootVersion.startsWith('1.4') +plugins { + id "io.spring.compatibility-test" version "0.0.4" + id "java-library" + id "maven-publish" } +description = "Spring REST Docs REST Assured" + dependencies { - compile project(':spring-restdocs-core') - compile 'com.jayway.restassured:rest-assured' + api(project(":spring-restdocs-core")) + api("io.rest-assured:rest-assured") + implementation("org.springframework:spring-web") + + internal(platform(project(":spring-restdocs-platform"))) - testCompile 'org.apache.tomcat.embed:tomcat-embed-core:8.5.11' - testCompile 'org.mockito:mockito-core' - testCompile 'org.hamcrest:hamcrest-library' - testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') + testCompileOnly("org.apiguardian:apiguardian-api") + testImplementation(testFixtures(project(":spring-restdocs-core"))) + testImplementation("com.fasterxml.jackson.core:jackson-databind") + testImplementation("org.apache.tomcat.embed:tomcat-embed-core") } -test { - jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" +tasks.named("test") { + useJUnitPlatform(); } -afterEvaluate { - if (project.hasProperty('platformVersion') && platformUsingBootOneFour) { - dependencies { - springIoTestRuntime 'org.springframework.boot:spring-boot-test' - } +compatibilityTest { + dependency("REST Assured") { restAssured -> + restAssured.groupId = "io.rest-assured" + restAssured.versions = ["5.3.+", "5.4.+", "5.5.+"] } } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java new file mode 100644 index 000000000..cef1e925e --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationPreprocessorsConfigurer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2022 the original author or 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. + */ + +package org.springframework.restdocs.restassured; + +import io.restassured.filter.Filter; +import io.restassured.filter.FilterContext; +import io.restassured.response.Response; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.FilterableResponseSpecification; + +import org.springframework.restdocs.config.OperationPreprocessorsConfigurer; + +/** + * A configurer that can be used to configure the operation preprocessors when using REST + * Assured. + * + * @author Filip Hrisafov + * @since 2.0.0 + */ +public final class RestAssuredOperationPreprocessorsConfigurer extends + OperationPreprocessorsConfigurer + implements Filter { + + RestAssuredOperationPreprocessorsConfigurer(RestAssuredRestDocumentationConfigurer parent) { + super(parent); + } + + @Override + public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, + FilterContext context) { + return and().filter(requestSpec, responseSpec, context); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index 5e44144ae..091d51ecc 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,14 +20,18 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Map.Entry; -import com.jayway.restassured.response.Header; -import com.jayway.restassured.specification.FilterableRequestSpecification; -import com.jayway.restassured.specification.MultiPartSpecification; +import io.restassured.http.Cookie; +import io.restassured.http.Header; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.MultiPartSpecification; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -36,30 +40,67 @@ import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationRequestPartFactory; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.util.FileCopyUtils; import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; /** * A converter for creating an {@link OperationRequest} from a REST Assured * {@link FilterableRequestSpecification}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ -class RestAssuredRequestConverter - implements RequestConverter { +class RestAssuredRequestConverter implements RequestConverter { @Override public OperationRequest convert(FilterableRequestSpecification requestSpec) { return new OperationRequestFactory().create(URI.create(requestSpec.getURI()), - HttpMethod.valueOf(requestSpec.getMethod().name()), - extractContent(requestSpec), extractHeaders(requestSpec), - extractParameters(requestSpec), extractParts(requestSpec)); + HttpMethod.valueOf(requestSpec.getMethod()), extractContent(requestSpec), extractHeaders(requestSpec), + extractParts(requestSpec), extractCookies(requestSpec)); + } + + private Collection extractCookies(FilterableRequestSpecification requestSpec) { + Collection cookies = new ArrayList<>(); + for (Cookie cookie : requestSpec.getCookies()) { + cookies.add(new RequestCookie(cookie.getName(), cookie.getValue())); + } + return cookies; } private byte[] extractContent(FilterableRequestSpecification requestSpec) { - return convertContent(requestSpec.getBody()); + Object body = requestSpec.getBody(); + if (body != null) { + return convertContent(body); + } + StringBuilder parameters = new StringBuilder(); + if ("POST".equals(requestSpec.getMethod())) { + appendParameters(parameters, requestSpec.getRequestParams()); + } + if (!"GET".equals(requestSpec.getMethod())) { + appendParameters(parameters, requestSpec.getFormParams()); + } + return parameters.toString().getBytes(StandardCharsets.ISO_8859_1); + } + + private void appendParameters(StringBuilder content, Map parameters) { + for (Entry entry : parameters.entrySet()) { + String name = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof Iterable) { + for (Object v : (Iterable) value) { + append(content, name, v.toString()); + } + } + else if (value != null) { + append(content, name, value.toString()); + } + else { + append(content, name); + } + } } private byte[] convertContent(Object content) { @@ -79,8 +120,7 @@ else if (content == null) { return new byte[0]; } else { - throw new IllegalStateException( - "Unsupported request content: " + content.getClass().getName()); + throw new IllegalStateException("Unsupported request content: " + content.getClass().getName()); } } @@ -89,8 +129,7 @@ private byte[] copyToByteArray(File file) { return FileCopyUtils.copyToByteArray(file); } catch (IOException ex) { - throw new IllegalStateException("Failed to read content from file " + file, - ex); + throw new IllegalStateException("Failed to read content from file " + file, ex); } } @@ -99,52 +138,63 @@ private byte[] copyToByteArray(InputStream inputStream) { inputStream.reset(); } catch (IOException ex) { - throw new IllegalStateException("Cannot read content from input stream " - + inputStream + " due to reset() failure"); + throw new IllegalStateException( + "Cannot read content from input stream " + inputStream + " due to reset() failure"); } try { return StreamUtils.copyToByteArray(inputStream); } catch (IOException ex) { - throw new IllegalStateException( - "Failed to read content from input stream " + inputStream, ex); + throw new IllegalStateException("Failed to read content from input stream " + inputStream, ex); } } private HttpHeaders extractHeaders(FilterableRequestSpecification requestSpec) { HttpHeaders httpHeaders = new HttpHeaders(); for (Header header : requestSpec.getHeaders()) { - httpHeaders.add(header.getName(), header.getValue()); + if (!isAllMediaTypesAcceptHeader(header)) { + httpHeaders.add(header.getName(), header.getValue()); + } } return httpHeaders; } - private Parameters extractParameters(FilterableRequestSpecification requestSpec) { - Parameters parameters = new Parameters(); - for (Entry entry : requestSpec.getQueryParams().entrySet()) { - parameters.add(entry.getKey(), entry.getValue().toString()); - } - for (Entry entry : requestSpec.getRequestParams().entrySet()) { - parameters.add(entry.getKey(), entry.getValue().toString()); - } - for (Entry entry : requestSpec.getFormParams().entrySet()) { - parameters.add(entry.getKey(), entry.getValue().toString()); - } - return parameters; + private boolean isAllMediaTypesAcceptHeader(Header header) { + return HttpHeaders.ACCEPT.equals(header.getName()) && "*/*".equals(header.getValue()); } - private Collection extractParts( - FilterableRequestSpecification requestSpec) { + private Collection extractParts(FilterableRequestSpecification requestSpec) { List parts = new ArrayList<>(); for (MultiPartSpecification multiPartSpec : requestSpec.getMultiPartParams()) { HttpHeaders headers = new HttpHeaders(); - headers.setContentType( - multiPartSpec.getMimeType() == null ? MediaType.TEXT_PLAIN - : MediaType.parseMediaType(multiPartSpec.getMimeType())); - parts.add(new OperationRequestPartFactory().create( - multiPartSpec.getControlName(), multiPartSpec.getFileName(), - convertContent(multiPartSpec.getContent()), headers)); + headers.setContentType((multiPartSpec.getMimeType() != null) + ? MediaType.parseMediaType(multiPartSpec.getMimeType()) : MediaType.TEXT_PLAIN); + parts.add(new OperationRequestPartFactory().create(multiPartSpec.getControlName(), + multiPartSpec.getFileName(), convertContent(multiPartSpec.getContent()), headers)); } return parts; } + + private static void append(StringBuilder sb, String key) { + append(sb, key, ""); + } + + private static void append(StringBuilder sb, String key, String value) { + doAppend(sb, urlEncode(key) + "=" + urlEncode(value)); + } + + private static void doAppend(StringBuilder sb, String toAppend) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(toAppend); + } + + private static String urlEncode(String s) { + if (!StringUtils.hasLength(s)) { + return ""; + } + return URLEncoder.encode(s, StandardCharsets.UTF_8); + } + } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java index 06e170992..a049ea1ae 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,28 +16,48 @@ package org.springframework.restdocs.restassured; -import com.jayway.restassured.response.Header; -import com.jayway.restassured.response.Response; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import io.restassured.http.Header; +import io.restassured.response.Response; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.operation.ResponseCookie; /** * A converter for creating an {@link OperationResponse} from a REST Assured * {@link Response}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ class RestAssuredResponseConverter implements ResponseConverter { @Override public OperationResponse convert(Response response) { - return new OperationResponseFactory().create( - HttpStatus.valueOf(response.getStatusCode()), extractHeaders(response), - extractContent(response)); + HttpHeaders headers = extractHeaders(response); + Collection cookies = extractCookies(response, headers); + return new OperationResponseFactory().create(HttpStatusCode.valueOf(response.getStatusCode()), + extractHeaders(response), extractContent(response), cookies); + } + + private Collection extractCookies(Response response, HttpHeaders headers) { + if (response.getCookies() == null || response.getCookies().size() == 0) { + return Collections.emptyList(); + } + List cookies = new ArrayList<>(); + for (Map.Entry cookie : response.getCookies().entrySet()) { + cookies.add(new ResponseCookie(cookie.getKey(), cookie.getValue())); + } + return cookies; } private HttpHeaders extractHeaders(Response response) { diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java index 96e412887..a5309d49d 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ * Static factory methods for documenting RESTful APIs using REST Assured. * * @author Andy Wilkinson - * @since 1.1.0 + * @since 1.2.0 */ public abstract class RestAssuredRestDocumentation { @@ -41,38 +41,34 @@ private RestAssuredRestDocumentation() { /** * Documents the API call with the given {@code identifier} using the given * {@code snippets}. - * * @param identifier an identifier for the API call that is being documented * @param snippets the snippets that will document the API call * @return a {@link RestDocumentationFilter} that will produce the documentation */ - public static RestDocumentationFilter document(String identifier, - Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, - REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); + public static RestDocumentationFilter document(String identifier, Snippet... snippets) { + return new RestDocumentationFilter( + new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); } /** * Documents the API call with the given {@code identifier} using the given * {@code snippets} in addition to any default snippets. The given * {@code requestPreprocessor} is applied to the request before it is documented. - * * @param identifier an identifier for the API call that is being documented * @param requestPreprocessor the request preprocessor * @param snippets the snippets * @return a {@link RestDocumentationFilter} that will produce the documentation */ - public static RestDocumentationFilter document(String identifier, - OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, - REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, snippets)); + public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, + Snippet... snippets) { + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, + RESPONSE_CONVERTER, requestPreprocessor, snippets)); } /** * Documents the API call with the given {@code identifier} using the given * {@code snippets} in addition to any default snippets. The given * {@code responsePreprocessor} is applied to the request before it is documented. - * * @param identifier an identifier for the API call that is being documented * @param responsePreprocessor the response preprocessor * @param snippets the snippets @@ -80,8 +76,8 @@ public static RestDocumentationFilter document(String identifier, */ public static RestDocumentationFilter document(String identifier, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, - REQUEST_CONVERTER, RESPONSE_CONVERTER, responsePreprocessor, snippets)); + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, + RESPONSE_CONVERTER, responsePreprocessor, snippets)); } /** @@ -89,25 +85,21 @@ public static RestDocumentationFilter document(String identifier, * {@code snippets} in addition to any default snippets. The given * {@code requestPreprocessor} and {@code responsePreprocessor} are applied to the * request and response respectively before they are documented. - * * @param identifier an identifier for the API call that is being documented * @param requestPreprocessor the request preprocessor * @param responsePreprocessor the response preprocessor * @param snippets the snippets * @return a {@link RestDocumentationFilter} that will produce the documentation */ - public static RestDocumentationFilter document(String identifier, - OperationRequestPreprocessor requestPreprocessor, + public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, - REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, - responsePreprocessor, snippets)); + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, + RESPONSE_CONVERTER, requestPreprocessor, responsePreprocessor, snippets)); } /** * Provides access to a {@link RestAssuredRestDocumentationConfigurer} that can be * used to configure Spring REST Docs using the given {@code contextProvider}. - * * @param contextProvider the context provider * @return the configurer */ diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java index e304d8d10..401d02b8c 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,11 @@ import java.util.HashMap; import java.util.Map; -import com.jayway.restassured.filter.Filter; -import com.jayway.restassured.filter.FilterContext; -import com.jayway.restassured.response.Response; -import com.jayway.restassured.specification.FilterableRequestSpecification; -import com.jayway.restassured.specification.FilterableResponseSpecification; +import io.restassured.filter.Filter; +import io.restassured.filter.FilterContext; +import io.restassured.response.Response; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.FilterableResponseSpecification; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.RestDocumentationContextProvider; @@ -33,19 +33,21 @@ * A REST Assured-specific {@link RestDocumentationConfigurer}. * * @author Andy Wilkinson - * @since 1.1.0 + * @author Filip Hrisafov + * @since 1.2.0 */ public final class RestAssuredRestDocumentationConfigurer extends - RestDocumentationConfigurer + RestDocumentationConfigurer implements Filter { - private final RestAssuredSnippetConfigurer snippetConfigurer = new RestAssuredSnippetConfigurer( + private final RestAssuredSnippetConfigurer snippetConfigurer = new RestAssuredSnippetConfigurer(this); + + private final RestAssuredOperationPreprocessorsConfigurer operationPreprocessorsConfigurer = new RestAssuredOperationPreprocessorsConfigurer( this); private final RestDocumentationContextProvider contextProvider; - RestAssuredRestDocumentationConfigurer( - RestDocumentationContextProvider contextProvider) { + RestAssuredRestDocumentationConfigurer(RestDocumentationContextProvider contextProvider) { this.contextProvider = contextProvider; } @@ -55,14 +57,19 @@ public RestAssuredSnippetConfigurer snippets() { } @Override - public Response filter(FilterableRequestSpecification requestSpec, - FilterableResponseSpecification responseSpec, FilterContext filterContext) { + public RestAssuredOperationPreprocessorsConfigurer operationPreprocessors() { + return this.operationPreprocessorsConfigurer; + } + + @Override + public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, + FilterContext filterContext) { RestDocumentationContext context = this.contextProvider.beforeOperation(); filterContext.setValue(RestDocumentationContext.class.getName(), context); Map configuration = new HashMap<>(); - filterContext.setValue(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION, - configuration); + filterContext.setValue(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION, configuration); apply(configuration, context); return filterContext.next(requestSpec, responseSpec); } + } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java index df98da51b..250f61fb8 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ package org.springframework.restdocs.restassured; -import com.jayway.restassured.filter.Filter; -import com.jayway.restassured.filter.FilterContext; -import com.jayway.restassured.response.Response; -import com.jayway.restassured.specification.FilterableRequestSpecification; -import com.jayway.restassured.specification.FilterableResponseSpecification; +import io.restassured.filter.Filter; +import io.restassured.filter.FilterContext; +import io.restassured.response.Response; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.FilterableResponseSpecification; import org.springframework.restdocs.config.SnippetConfigurer; @@ -29,19 +29,18 @@ * using REST Assured. * * @author Andy Wilkinson - * @since 1.1.0 + * @since 1.2.0 */ public final class RestAssuredSnippetConfigurer extends - SnippetConfigurer - implements Filter { + SnippetConfigurer implements Filter { RestAssuredSnippetConfigurer(RestAssuredRestDocumentationConfigurer parent) { super(parent); } @Override - public Response filter(FilterableRequestSpecification requestSpec, - FilterableResponseSpecification responseSpec, FilterContext context) { + public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, + FilterContext context) { return and().filter(requestSpec, responseSpec, context); } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java index 035711a92..e52b9c313 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,11 @@ import java.util.HashMap; import java.util.Map; -import com.jayway.restassured.filter.Filter; -import com.jayway.restassured.filter.FilterContext; -import com.jayway.restassured.response.Response; -import com.jayway.restassured.specification.FilterableRequestSpecification; -import com.jayway.restassured.specification.FilterableResponseSpecification; +import io.restassured.filter.Filter; +import io.restassured.filter.FilterContext; +import io.restassured.response.Response; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.FilterableResponseSpecification; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.generate.RestDocumentationGenerator; @@ -34,7 +34,7 @@ * A REST Assured {@link Filter} for documenting RESTful APIs. * * @author Andy Wilkinson - * @since 1.1.0 + * @since 1.2.0 */ public class RestDocumentationFilter implements Filter { @@ -42,8 +42,7 @@ public class RestDocumentationFilter implements Filter { private final RestDocumentationGenerator delegate; - RestDocumentationFilter( - RestDocumentationGenerator delegate) { + RestDocumentationFilter(RestDocumentationGenerator delegate) { Assert.notNull(delegate, "delegate must be non-null"); this.delegate = delegate; } @@ -63,41 +62,21 @@ public final Response filter(FilterableRequestSpecification requestSpec, /** * Returns the configuration that should be used when calling the delgate. The * configuration is derived from the given {@code requestSpec} and {@code context}. - * * @param requestSpec the request specification * @param context the filter context * @return the configuration */ - protected Map getConfiguration( - FilterableRequestSpecification requestSpec, FilterContext context) { - Map configuration = new HashMap<>( - context.>getValue(CONTEXT_KEY_CONFIGURATION)); + protected Map getConfiguration(FilterableRequestSpecification requestSpec, FilterContext context) { + Map configuration = new HashMap<>(retrieveConfiguration(context)); configuration.put(RestDocumentationContext.class.getName(), - context.getValue( - RestDocumentationContext.class.getName())); - configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - requestSpec.getUserDefinedPath()); + context.getValue(RestDocumentationContext.class.getName())); + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, requestSpec.getUserDefinedPath()); return configuration; } - /** - * Adds the given {@code snippets} such that they are documented when this result - * handler is called. - * - * @param snippets the snippets to add - * @return this {@code RestDocumentationFilter} - * @deprecated since 1.1 in favor of {@link #document(Snippet...)} - */ - @Deprecated - public final RestDocumentationFilter snippets(Snippet... snippets) { - this.delegate.addSnippets(snippets); - return this; - } - /** * Creates a new {@link RestDocumentationFilter} that will produce documentation using * the given {@code snippets}. - * * @param snippets the snippets * @return the new result handler */ @@ -105,16 +84,25 @@ public final RestDocumentationFilter document(Snippet... snippets) { return new RestDocumentationFilter(this.delegate.withSnippets(snippets)) { @Override - protected Map getConfiguration( - FilterableRequestSpecification requestSpec, FilterContext context) { - Map configuration = super.getConfiguration(requestSpec, - context); - configuration.remove( - RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + protected Map getConfiguration(FilterableRequestSpecification requestSpec, + FilterContext context) { + Map configuration = super.getConfiguration(requestSpec, context); + configuration.remove(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + configuration.remove(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_REQUEST_PREPROCESSOR); + configuration.remove(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR); return configuration; } }; } + private static Map retrieveConfiguration(FilterContext context) { + Map configuration = context.getValue(CONTEXT_KEY_CONFIGURATION); + Assert.state(configuration != null, + () -> "REST Docs configuration not found. Did you forget to add a " + + RestAssuredRestDocumentationConfigurer.class.getSimpleName() + + " as a filter when building the RequestSpecification?"); + return configuration; + } + } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java deleted file mode 100644 index 1579e89ad..000000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -package org.springframework.restdocs.restassured.operation.preprocess; - -import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationResponse; - -/** - * Static factory methods for creating - * {@link org.springframework.restdocs.operation.preprocess.OperationPreprocessor - * OperationPreprocessors} for use with REST Assured. They can be applied to an - * {@link Operation Operation's} {@link OperationRequest request} or - * {@link OperationResponse response} before it is documented. - * - * @author Andy Wilkinson - * @since 1.1.0 - */ -public abstract class RestAssuredPreprocessors { - - private RestAssuredPreprocessors() { - - } - - /** - * Returns a {@code UriModifyingOperationPreprocessor} that will modify URIs in the - * request or response by changing one or more of their host, scheme, and port. - * - * @return the preprocessor - */ - public static UriModifyingOperationPreprocessor modifyUris() { - return new UriModifyingOperationPreprocessor(); - } - -} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java deleted file mode 100644 index 57a3c5284..000000000 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2014-2016 the original author or 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. - */ - -/** - * REST Assured-specific support for preprocessing an operation prior to it being - * documented. - */ -package org.springframework.restdocs.restassured.operation.preprocess; diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java index 3c686030a..502b9a02c 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredParameterBehaviorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredParameterBehaviorTests.java new file mode 100644 index 000000000..1bef29294 --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredParameterBehaviorTests.java @@ -0,0 +1,263 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.restassured; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; +import org.assertj.core.api.AbstractAssert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.OperationRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests to verify that the understanding of REST Assured's parameter handling behavior is + * correct. + * + * @author Andy Wilkinson + */ +class RestAssuredParameterBehaviorTests { + + private static final MediaType APPLICATION_FORM_URLENCODED_ISO_8859_1 = MediaType + .parseMediaType(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=ISO-8859-1"); + + @RegisterExtension + public static TomcatServer tomcat = new TomcatServer(); + + private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); + + private OperationRequest request; + + private RequestSpecification spec = RestAssured.given() + .port(tomcat.getPort()) + .filter((request, response, context) -> { + this.request = this.factory.convert(request); + return context.next(request, response); + }); + + @Test + void queryParameterOnGet() { + this.spec.queryParam("a", "alpha", "apple") + .queryParam("b", "bravo") + .get("/query-parameter") + .then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.GET); + } + + @Test + void queryParameterOnHead() { + this.spec.queryParam("a", "alpha", "apple") + .queryParam("b", "bravo") + .head("/query-parameter") + .then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.HEAD); + } + + @Test + void queryParameterOnPost() { + this.spec.queryParam("a", "alpha", "apple") + .queryParam("b", "bravo") + .post("/query-parameter") + .then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.POST); + } + + @Test + void queryParameterOnPut() { + this.spec.queryParam("a", "alpha", "apple") + .queryParam("b", "bravo") + .put("/query-parameter") + .then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.PUT); + } + + @Test + void queryParameterOnPatch() { + this.spec.queryParam("a", "alpha", "apple") + .queryParam("b", "bravo") + .patch("/query-parameter") + .then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.PATCH); + } + + @Test + void queryParameterOnDelete() { + this.spec.queryParam("a", "alpha", "apple") + .queryParam("b", "bravo") + .delete("/query-parameter") + .then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.DELETE); + } + + @Test + void queryParameterOnOptions() { + this.spec.queryParam("a", "alpha", "apple") + .queryParam("b", "bravo") + .options("/query-parameter") + .then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.OPTIONS); + } + + @Test + void paramOnGet() { + this.spec.param("a", "alpha", "apple").param("b", "bravo").get("/query-parameter").then().statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.GET); + } + + @Test + void paramOnHead() { + this.spec.param("a", "alpha", "apple").param("b", "bravo").head("/query-parameter").then().statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.HEAD); + } + + @Test + void paramOnPost() { + this.spec.param("a", "alpha", "apple").param("b", "bravo").post("/form-url-encoded").then().statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.POST); + } + + @Test + void paramOnPut() { + this.spec.param("a", "alpha", "apple").param("b", "bravo").put("/query-parameter").then().statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.PUT); + } + + @Test + void paramOnPatch() { + this.spec.param("a", "alpha", "apple").param("b", "bravo").patch("/query-parameter").then().statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.PATCH); + } + + @Test + void paramOnDelete() { + this.spec.param("a", "alpha", "apple").param("b", "bravo").delete("/query-parameter").then().statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.DELETE); + } + + @Test + void paramOnOptions() { + this.spec.param("a", "alpha", "apple").param("b", "bravo").options("/query-parameter").then().statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.OPTIONS); + } + + @Test + void formParamOnGet() { + this.spec.formParam("a", "alpha", "apple") + .formParam("b", "bravo") + .get("/query-parameter") + .then() + .statusCode(200); + assertThatRequest(this.request).hasQueryParametersWithMethod(HttpMethod.GET); + } + + @Test + void formParamOnHead() { + this.spec.formParam("a", "alpha", "apple") + .formParam("b", "bravo") + .head("/form-url-encoded") + .then() + .statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.HEAD); + } + + @Test + void formParamOnPost() { + this.spec.formParam("a", "alpha", "apple") + .formParam("b", "bravo") + .post("/form-url-encoded") + .then() + .statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.POST); + } + + @Test + void formParamOnPut() { + this.spec.formParam("a", "alpha", "apple") + .formParam("b", "bravo") + .put("/form-url-encoded") + .then() + .statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.PUT); + } + + @Test + void formParamOnPatch() { + this.spec.formParam("a", "alpha", "apple") + .formParam("b", "bravo") + .patch("/form-url-encoded") + .then() + .statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.PATCH); + } + + @Test + void formParamOnDelete() { + this.spec.formParam("a", "alpha", "apple") + .formParam("b", "bravo") + .delete("/form-url-encoded") + .then() + .statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.DELETE); + } + + @Test + void formParamOnOptions() { + this.spec.formParam("a", "alpha", "apple") + .formParam("b", "bravo") + .options("/form-url-encoded") + .then() + .statusCode(200); + assertThatRequest(this.request).isFormUrlEncodedWithMethod(HttpMethod.OPTIONS); + } + + private OperationRequestAssert assertThatRequest(OperationRequest request) { + return new OperationRequestAssert(request); + } + + private static final class OperationRequestAssert extends AbstractAssert { + + private OperationRequestAssert(OperationRequest actual) { + super(actual, OperationRequestAssert.class); + } + + private void isFormUrlEncodedWithMethod(HttpMethod method) { + assertThat(this.actual.getMethod()).isEqualTo(method); + assertThat(this.actual.getUri().getRawQuery()).isNull(); + assertThat(this.actual.getContentAsString()).isEqualTo("a=alpha&a=apple&b=bravo"); + assertThat(this.actual.getHeaders().getContentType()).isEqualTo(APPLICATION_FORM_URLENCODED_ISO_8859_1); + } + + private void hasQueryParametersWithMethod(HttpMethod method) { + assertThat(this.actual.getMethod()).isEqualTo(method); + assertThat(this.actual.getUri().getRawQuery()).isEqualTo("a=alpha&a=apple&b=bravo"); + assertThat(this.actual.getContentAsString()).isEmpty(); + } + + } + +} diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 043795413..09d411ae2 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,274 +21,257 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.net.URI; -import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; -import com.jayway.restassured.RestAssured; -import com.jayway.restassured.specification.FilterableRequestSpecification; -import com.jayway.restassured.specification.RequestSpecification; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; +import io.restassured.RestAssured; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.RequestCookie; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link RestAssuredRequestConverter}. * * @author Andy Wilkinson */ -public class RestAssuredRequestConverterTests { +class RestAssuredRequestConverterTests { - @ClassRule + @RegisterExtension public static TomcatServer tomcat = new TomcatServer(); - @Rule - public final ExpectedException thrown = ExpectedException.none(); - private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); @Test - public void requestUri() { + void requestUri() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); requestSpec.get("/foo/bar"); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getUri(), is(equalTo( - URI.create("http://localhost:" + tomcat.getPort() + "/foo/bar")))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:" + tomcat.getPort() + "/foo/bar")); } @Test - public void requestMethod() { + void requestMethod() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); requestSpec.head("/foo/bar"); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getMethod(), is(equalTo(HttpMethod.HEAD))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getMethod()).isEqualTo(HttpMethod.HEAD); } @Test - public void queryStringParameters() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .queryParam("foo", "bar"); + void queryStringParameters() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).queryParam("foo", "bar"); requestSpec.get("/"); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParameters().size(), is(1)); - assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getUri().getRawQuery()).isEqualTo("foo=bar"); } @Test - public void queryStringFromUrlParameters() { + void queryStringFromUrlParameters() { RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); - requestSpec.get("/?foo=bar"); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParameters().size(), is(1)); - assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); + requestSpec.get("/?foo=bar&foo=qix"); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getUri().getRawQuery()).isEqualTo("foo=bar&foo=qix"); } @Test - public void formParameters() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .formParameter("foo", "bar"); + void paramOnGetRequestIsMappedToQueryString() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).param("foo", "bar"); requestSpec.get("/"); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParameters().size(), is(1)); - assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getUri().getRawQuery()).isEqualTo("foo=bar"); } @Test - public void requestParameters() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .parameter("foo", "bar"); + void headers() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).header("Foo", "bar"); requestSpec.get("/"); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParameters().size(), is(1)); - assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getHeaders().headerSet()).containsOnly(entry("Foo", Collections.singletonList("bar")), + entry("Host", Collections.singletonList("localhost:" + tomcat.getPort()))); } @Test - public void headers() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .header("Foo", "bar"); + void headersWithCustomAccept() { + RequestSpecification requestSpec = RestAssured.given() + .port(tomcat.getPort()) + .header("Foo", "bar") + .accept("application/json"); requestSpec.get("/"); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getHeaders().toString(), request.getHeaders().size(), is(3)); - assertThat(request.getHeaders().get("Foo"), is(equalTo(Arrays.asList("bar")))); - assertThat(request.getHeaders().get("Accept"), is(equalTo(Arrays.asList("*/*")))); - assertThat(request.getHeaders().get("Host"), - is(equalTo(Arrays.asList("localhost:" + tomcat.getPort())))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getHeaders().headerSet()).containsOnly(entry("Foo", Collections.singletonList("bar")), + entry("Accept", Collections.singletonList("application/json")), + entry("Host", Collections.singletonList("localhost:" + tomcat.getPort()))); + } + + @Test + void cookies() { + RequestSpecification requestSpec = RestAssured.given() + .port(tomcat.getPort()) + .cookie("cookie1", "cookieVal1") + .cookie("cookie2", "cookieVal2"); + requestSpec.get("/"); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getCookies().size()).isEqualTo(2); + + Iterator cookieIterator = request.getCookies().iterator(); + RequestCookie cookie1 = cookieIterator.next(); + + assertThat(cookie1.getName()).isEqualTo("cookie1"); + assertThat(cookie1.getValue()).isEqualTo("cookieVal1"); + + RequestCookie cookie2 = cookieIterator.next(); + assertThat(cookie2.getName()).isEqualTo("cookie2"); + assertThat(cookie2.getValue()).isEqualTo("cookieVal2"); } @Test - public void multipart() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .multiPart("a", "a.txt", "alpha", null) - .multiPart("b", new ObjectBody("bar"), "application/json"); + void multipart() { + RequestSpecification requestSpec = RestAssured.given() + .port(tomcat.getPort()) + .multiPart("a", "a.txt", "alpha", null) + .multiPart("b", new ObjectBody("bar"), "application/json"); requestSpec.post(); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); Collection parts = request.getParts(); - assertThat(parts.size(), is(2)); - Iterator iterator = parts.iterator(); - OperationRequestPart part = iterator.next(); - assertThat(part.getName(), is(equalTo("a"))); - assertThat(part.getSubmittedFileName(), is(equalTo("a.txt"))); - assertThat(part.getContentAsString(), is(equalTo("alpha"))); - assertThat(part.getHeaders().getContentType(), is(equalTo(MediaType.TEXT_PLAIN))); - part = iterator.next(); - assertThat(part.getName(), is(equalTo("b"))); - assertThat(part.getSubmittedFileName(), is(equalTo("file"))); - assertThat(part.getContentAsString(), is(equalTo("{\"foo\":\"bar\"}"))); - assertThat(part.getHeaders().getContentType(), - is(equalTo(MediaType.APPLICATION_JSON))); + assertThat(parts).hasSize(2); + assertThat(parts).extracting("name").containsExactly("a", "b"); + assertThat(parts).extracting("submittedFileName").containsExactly("a.txt", "file"); + assertThat(parts).extracting("contentAsString").containsExactly("alpha", "{\"foo\":\"bar\"}"); + assertThat(parts).map((part) -> part.getHeaders().get(HttpHeaders.CONTENT_TYPE)) + .containsExactly(Collections.singletonList(MediaType.TEXT_PLAIN_VALUE), + Collections.singletonList(MediaType.APPLICATION_JSON_VALUE)); } @Test - public void byteArrayBody() { - RequestSpecification requestSpec = RestAssured.given().body("body".getBytes()) - .port(tomcat.getPort()); + void byteArrayBody() { + RequestSpecification requestSpec = RestAssured.given().body("body".getBytes()).port(tomcat.getPort()); requestSpec.post(); this.factory.convert((FilterableRequestSpecification) requestSpec); } @Test - public void stringBody() { - RequestSpecification requestSpec = RestAssured.given().body("body") - .port(tomcat.getPort()); + void stringBody() { + RequestSpecification requestSpec = RestAssured.given().body("body").port(tomcat.getPort()); requestSpec.post(); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getContentAsString(), is(equalTo("body"))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getContentAsString()).isEqualTo("body"); } @Test - public void objectBody() { - RequestSpecification requestSpec = RestAssured.given().body(new ObjectBody("bar")) - .port(tomcat.getPort()); + void objectBody() { + RequestSpecification requestSpec = RestAssured.given().body(new ObjectBody("bar")).port(tomcat.getPort()); requestSpec.post(); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getContentAsString(), is(equalTo("{\"foo\":\"bar\"}"))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getContentAsString()).isEqualTo("{\"foo\":\"bar\"}"); } @Test - public void byteArrayInputStreamBody() { + void byteArrayInputStreamBody() { RequestSpecification requestSpec = RestAssured.given() - .body(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })) - .port(tomcat.getPort()); + .body(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })) + .port(tomcat.getPort()); requestSpec.post(); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getContent()).isEqualTo(new byte[] { 1, 2, 3, 4 }); } @Test - public void fileBody() { + void fileBody() { RequestSpecification requestSpec = RestAssured.given() - .body(new File("src/test/resources/body.txt")).port(tomcat.getPort()); + .body(new File("src/test/resources/body.txt")) + .port(tomcat.getPort()); requestSpec.post(); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getContentAsString(), is(equalTo("file"))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getContentAsString()).isEqualTo("file"); } @Test - public void fileInputStreamBody() throws FileNotFoundException { + void fileInputStreamBody() throws FileNotFoundException { FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); - RequestSpecification requestSpec = RestAssured.given().body(inputStream) - .port(tomcat.getPort()); + RequestSpecification requestSpec = RestAssured.given().body(inputStream).port(tomcat.getPort()); requestSpec.post(); - this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Cannot read content from input stream " + inputStream - + " due to reset() failure"); - this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThatIllegalStateException() + .isThrownBy(() -> this.factory.convert((FilterableRequestSpecification) requestSpec)) + .withMessage("Cannot read content from input stream " + inputStream + " due to reset() failure"); } @Test - public void multipartWithByteArrayInputStreamBody() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .multiPart("foo", "foo.txt", new ByteArrayInputStream("foo".getBytes())); + void multipartWithByteArrayInputStreamBody() { + RequestSpecification requestSpec = RestAssured.given() + .port(tomcat.getPort()) + .multiPart("foo", "foo.txt", new ByteArrayInputStream("foo".getBytes())); requestSpec.post(); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParts().iterator().next().getContentAsString(), - is(equalTo("foo"))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("foo"); } @Test - public void multipartWithStringBody() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .multiPart("control", "foo"); + void multipartWithStringBody() { + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()).multiPart("control", "foo"); requestSpec.post(); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParts().iterator().next().getContentAsString(), - is(equalTo("foo"))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("foo"); } @Test - public void multipartWithByteArrayBody() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .multiPart("control", "file", "foo".getBytes()); + void multipartWithByteArrayBody() { + RequestSpecification requestSpec = RestAssured.given() + .port(tomcat.getPort()) + .multiPart("control", "file", "foo".getBytes()); requestSpec.post(); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParts().iterator().next().getContentAsString(), - is(equalTo("foo"))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("foo"); } @Test - public void multipartWithFileBody() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .multiPart(new File("src/test/resources/body.txt")); + void multipartWithFileBody() { + RequestSpecification requestSpec = RestAssured.given() + .port(tomcat.getPort()) + .multiPart(new File("src/test/resources/body.txt")); requestSpec.post(); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParts().iterator().next().getContentAsString(), - is(equalTo("file"))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("file"); } @Test - public void multipartWithFileInputStreamBody() throws FileNotFoundException { + void multipartWithFileInputStreamBody() throws FileNotFoundException { FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .multiPart("foo", "foo.txt", inputStream); + RequestSpecification requestSpec = RestAssured.given() + .port(tomcat.getPort()) + .multiPart("foo", "foo.txt", inputStream); requestSpec.post(); - this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Cannot read content from input stream " + inputStream - + " due to reset() failure"); - this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThatIllegalStateException() + .isThrownBy(() -> this.factory.convert((FilterableRequestSpecification) requestSpec)) + .withMessage("Cannot read content from input stream " + inputStream + " due to reset() failure"); } @Test - public void multipartWithObjectBody() { - RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) - .multiPart("control", new ObjectBody("bar")); + void multipartWithObjectBody() { + RequestSpecification requestSpec = RestAssured.given() + .port(tomcat.getPort()) + .multiPart("control", new ObjectBody("bar")); requestSpec.post(); - OperationRequest request = this.factory - .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getParts().iterator().next().getContentAsString(), - is(equalTo("{\"foo\":\"bar\"}"))); + OperationRequest request = this.factory.convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString()).isEqualTo("{\"foo\":\"bar\"}"); } /** * Sample object body to verify JSON serialization. */ - static class ObjectBody { + public static class ObjectBody { private final String foo; diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java new file mode 100644 index 000000000..22b17601b --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredResponseConverterTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.restassured; + +import io.restassured.http.Headers; +import io.restassured.response.Response; +import io.restassured.response.ResponseBody; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpStatusCode; +import org.springframework.restdocs.operation.OperationResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RestAssuredResponseConverter}. + * + * @author Andy Wilkinson + */ +class RestAssuredResponseConverterTests { + + private final RestAssuredResponseConverter converter = new RestAssuredResponseConverter(); + + @Test + void responseWithCustomStatus() { + Response response = mock(Response.class); + given(response.getStatusCode()).willReturn(600); + given(response.getHeaders()).willReturn(new Headers()); + ResponseBody body = mock(ResponseBody.class); + given(response.getBody()).willReturn(body); + given(body.asByteArray()).willReturn(new byte[0]); + OperationResponse operationResponse = this.converter.convert(response); + assertThat(operationResponse.getStatus()).isEqualTo(HttpStatusCode.valueOf(600)); + } + +} diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index f208a83cc..8dbf59354 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,23 +19,25 @@ import java.util.List; import java.util.Map; -import com.jayway.restassured.filter.FilterContext; -import com.jayway.restassured.specification.FilterableRequestSpecification; -import com.jayway.restassured.specification.FilterableResponseSpecification; -import org.junit.Rule; -import org.junit.Test; +import io.restassured.filter.FilterContext; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.FilterableResponseSpecification; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.generate.RestDocumentationGenerator; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.operation.preprocess.Preprocessors; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.TemplateEngine; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.Matchers.hasEntry; -import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.eq; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -43,47 +45,50 @@ * Tests for {@link RestAssuredRestDocumentationConfigurer}. * * @author Andy Wilkinson + * @author Filip Hrisafov */ -public class RestAssuredRestDocumentationConfigurerTests { +@ExtendWith(RestDocumentationExtension.class) +class RestAssuredRestDocumentationConfigurerTests { - @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build"); + private final FilterableRequestSpecification requestSpec = mock(FilterableRequestSpecification.class); - private final FilterableRequestSpecification requestSpec = mock( - FilterableRequestSpecification.class); - - private final FilterableResponseSpecification responseSpec = mock( - FilterableResponseSpecification.class); + private final FilterableResponseSpecification responseSpec = mock(FilterableResponseSpecification.class); private final FilterContext filterContext = mock(FilterContext.class); - private final RestAssuredRestDocumentationConfigurer configurer = new RestAssuredRestDocumentationConfigurer( - this.restDocumentation); + private RestAssuredRestDocumentationConfigurer configurer; + + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + this.configurer = new RestAssuredRestDocumentationConfigurer(restDocumentation); + } @Test - public void nextFilterIsCalled() { + void nextFilterIsCalled() { this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); verify(this.filterContext).next(this.requestSpec, this.responseSpec); } @Test - public void configurationIsAddedToTheContext() { - this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); + void configurationIsAddedToTheContext() { + this.configurer.operationPreprocessors() + .withRequestDefaults(Preprocessors.prettyPrint()) + .withResponseDefaults(Preprocessors.modifyHeaders().remove("Foo")) + .filter(this.requestSpec, this.responseSpec, this.filterContext); @SuppressWarnings("rawtypes") ArgumentCaptor configurationCaptor = ArgumentCaptor.forClass(Map.class); - verify(this.filterContext).setValue( - eq(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION), + verify(this.filterContext).setValue(eq(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION), configurationCaptor.capture()); @SuppressWarnings("unchecked") Map configuration = configurationCaptor.getValue(); - assertThat(configuration, hasEntry(equalTo(TemplateEngine.class.getName()), - instanceOf(TemplateEngine.class))); - assertThat(configuration, hasEntry(equalTo(WriterResolver.class.getName()), - instanceOf(WriterResolver.class))); - assertThat(configuration, - hasEntry( - equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), - instanceOf(List.class))); + assertThat(configuration.get(TemplateEngine.class.getName())).isInstanceOf(TemplateEngine.class); + assertThat(configuration.get(WriterResolver.class.getName())).isInstanceOf(WriterResolver.class); + assertThat(configuration.get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS)) + .isInstanceOf(List.class); + assertThat(configuration.get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_REQUEST_PREPROCESSOR)) + .isInstanceOf(OperationRequestPreprocessor.class); + assertThat(configuration.get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_OPERATION_RESPONSE_PREPROCESSOR)) + .isInstanceOf(OperationResponsePreprocessor.class); } + } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 832f262d3..89d9c3516 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,329 +17,498 @@ package org.springframework.restdocs.restassured; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; -import com.jayway.restassured.builder.RequestSpecBuilder; -import com.jayway.restassured.specification.RequestSpecification; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.testfixtures.SnippetConditions; +import org.springframework.restdocs.testfixtures.SnippetConditions.CodeBlockCondition; +import org.springframework.restdocs.testfixtures.SnippetConditions.HttpRequestCondition; +import org.springframework.restdocs.testfixtures.SnippetConditions.HttpResponseCondition; +import org.springframework.util.FileCopyUtils; import org.springframework.web.bind.annotation.RequestMethod; -import static com.jayway.restassured.RestAssured.given; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.operation.preprocess.Preprocessors.maskLinks; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.replacePattern; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris; -import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; -import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; -import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; -import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; -import static org.springframework.restdocs.test.SnippetMatchers.snippet; /** * Integration tests for using Spring REST Docs with REST Assured. * * @author Andy Wilkinson + * @author Tomasz Kopczynski + * @author Filip Hrisafov */ -public class RestAssuredRestDocumentationIntegrationTests { +@ExtendWith(RestDocumentationExtension.class) +class RestAssuredRestDocumentationIntegrationTests { - @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build/generated-snippets"); + @RegisterExtension + private static TomcatServer tomcat = new TomcatServer(); - @ClassRule - public static TomcatServer tomcat = new TomcatServer(); + @Test + void defaultSnippetGeneration(RestDocumentationContextProvider restDocumentation) { + given().port(tomcat.getPort()) + .filter(documentationConfiguration(restDocumentation)) + .filter(document("default")) + .get("/") + .then() + .statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/default"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + } @Test - public void defaultSnippetGeneration() { + void curlSnippetWithContent(RestDocumentationContextProvider restDocumentation) { + String contentType = "text/plain; charset=UTF-8"; given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("default")).get("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/default"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc"); + .filter(documentationConfiguration(restDocumentation)) + .filter(document("curl-snippet-with-content")) + .accept("application/json") + .body("content") + .contentType(contentType) + .post("/") + .then() + .statusCode(200); + + assertThat(new File("build/generated-snippets/curl-snippet-with-content/curl-request.adoc")).has(content( + codeBlock(TemplateFormats.asciidoctor(), "bash").withContent(String.format("$ curl 'http://localhost:" + + tomcat.getPort() + "/' -i -X POST \\%n" + " -H 'Accept: application/json' \\%n" + + " -H 'Content-Type: " + contentType + "' \\%n" + " -d 'content'")))); } @Test - public void curlSnippetWithContent() throws Exception { + void curlSnippetWithCookies(RestDocumentationContextProvider restDocumentation) { String contentType = "text/plain; charset=UTF-8"; given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("curl-snippet-with-content")).accept("application/json") - .content("content").contentType(contentType).post("/").then() - .statusCode(200); + .filter(documentationConfiguration(restDocumentation)) + .filter(document("curl-snippet-with-cookies")) + .accept("application/json") + .contentType(contentType) + .cookie("cookieName", "cookieVal") + .get("/") + .then() + .statusCode(200); + assertThat(new File("build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc")).has(content( + codeBlock(TemplateFormats.asciidoctor(), "bash").withContent(String.format("$ curl 'http://localhost:" + + tomcat.getPort() + "/' -i -X GET \\%n" + " -H 'Accept: application/json' \\%n" + + " -H 'Content-Type: " + contentType + "' \\%n" + " --cookie 'cookieName=cookieVal'")))); + } + @Test + void curlSnippetWithEmptyParameterQueryString(RestDocumentationContextProvider restDocumentation) { + given().port(tomcat.getPort()) + .filter(documentationConfiguration(restDocumentation)) + .filter(document("curl-snippet-with-empty-parameter-query-string")) + .accept("application/json") + .param("a", "") + .get("/") + .then() + .statusCode(200); assertThat( - new File( - "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), - is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") - .content("$ curl 'http://localhost:" + tomcat.getPort() + "/' -i " - + "-X POST -H 'Accept: application/json' " - + "-H 'Content-Type: " + contentType + "' " - + "-d 'content'")))); + new File("build/generated-snippets/curl-snippet-with-empty-parameter-query-string/curl-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ curl 'http://localhost:" + tomcat.getPort() + + "/?a=' -i -X GET \\%n -H 'Accept: application/json'")))); } @Test - public void curlSnippetWithQueryStringOnPost() throws Exception { + void curlSnippetWithQueryStringOnPost(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("curl-snippet-with-query-string")) - .accept("application/json").param("foo", "bar").param("a", "alpha") - .post("/?foo=bar").then().statusCode(200); + .filter(documentationConfiguration(restDocumentation)) + .filter(document("curl-snippet-with-query-string")) + .accept("application/json") + .param("foo", "bar") + .param("a", "alpha") + .post("/?foo=bar") + .then() + .statusCode(200); String contentType = "application/x-www-form-urlencoded; charset=ISO-8859-1"; - assertThat( - new File( - "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), - is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") - .content("$ curl " + "'http://localhost:" + tomcat.getPort() - + "/?foo=bar' -i -X POST " - + "-H 'Accept: application/json' " + "-H 'Content-Type: " - + contentType + "' " + "-d 'a=alpha'")))); + assertThat(new File("build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ curl " + "'http://localhost:" + tomcat.getPort() + + "/?foo=bar' -i -X POST \\%n" + " -H 'Accept: application/json' \\%n" + + " -H 'Content-Type: " + contentType + "' \\%n" + " -d 'foo=bar&a=alpha'")))); } @Test - public void linksSnippet() throws Exception { + void linksSnippet(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("links", - links(linkWithRel("rel").description("The description")))) - .accept("application/json").get("/").then().statusCode(200); - assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc", - "links.adoc"); + .filter(documentationConfiguration(restDocumentation)) + .filter(document("links", links(linkWithRel("rel").description("The description")))) + .accept("application/json") + .get("/") + .then() + .statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "links.adoc"); } @Test - public void pathParametersSnippet() throws Exception { + void pathParametersSnippet(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("path-parameters", - pathParameters( - parameterWithName("foo").description("The description")))) - .accept("application/json").get("/{foo}", "").then().statusCode(200); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/path-parameters"), "http-request.adoc", + .filter(documentationConfiguration(restDocumentation)) + .filter(document("path-parameters", + pathParameters(parameterWithName("foo").description("The description")))) + .accept("application/json") + .get("/{foo}", "") + .then() + .statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/path-parameters"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", "path-parameters.adoc"); } @Test - public void requestParametersSnippet() throws Exception { + void queryParametersSnippet(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("request-parameters", - requestParameters( - parameterWithName("foo").description("The description")))) - .accept("application/json").param("foo", "bar").get("/").then() - .statusCode(200); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/request-parameters"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc", - "request-parameters.adoc"); + .filter(documentationConfiguration(restDocumentation)) + .filter(document("query-parameters", + queryParameters(parameterWithName("foo").description("The description")))) + .accept("application/json") + .param("foo", "bar") + .get("/") + .then() + .statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/query-parameters"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "query-parameters.adoc"); } @Test - public void requestFieldsSnippet() throws Exception { + void requestFieldsSnippet(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("request-fields", - requestFields(fieldWithPath("a").description("The description")))) - .accept("application/json").content("{\"a\":\"alpha\"}").post("/").then() - .statusCode(200); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/request-fields"), "http-request.adoc", + .filter(documentationConfiguration(restDocumentation)) + .filter(document("request-fields", requestFields(fieldWithPath("a").description("The description")))) + .accept("application/json") + .body("{\"a\":\"alpha\"}") + .post("/") + .then() + .statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/request-fields"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", "request-fields.adoc"); } @Test - public void requestPartsSnippet() throws Exception { + void requestPartsSnippet(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("request-parts", - requestParts(partWithName("a").description("The description")))) - .multiPart("a", "foo").post("/upload").then().statusCode(200); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/request-parts"), "http-request.adoc", + .filter(documentationConfiguration(restDocumentation)) + .filter(document("request-parts", requestParts(partWithName("a").description("The description")))) + .multiPart("a", "foo") + .post("/upload") + .then() + .statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/request-parts"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", "request-parts.adoc"); } @Test - public void responseFieldsSnippet() throws Exception { + void responseFieldsSnippet(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("response-fields", - responseFields(fieldWithPath("a").description("The description"), - fieldWithPath("links") - .description("Links to other resources")))) - .accept("application/json").get("/").then().statusCode(200); - - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/response-fields"), "http-request.adoc", + .filter(documentationConfiguration(restDocumentation)) + .filter(document("response-fields", + responseFields(fieldWithPath("a").description("The description"), + subsectionWithPath("links").description("Links to other resources")))) + .accept("application/json") + .get("/") + .then() + .statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/response-fields"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", "response-fields.adoc"); } @Test - public void parameterizedOutputDirectory() throws Exception { + void parameterizedOutputDirectory(RestDocumentationContextProvider restDocumentation) { given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("{method-name}")).get("/").then().statusCode(200); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/parameterized-output-directory"), + .filter(documentationConfiguration(restDocumentation)) + .filter(document("{method-name}")) + .get("/") + .then() + .statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/parameterized-output-directory"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } @Test - public void multiStep() throws Exception { + void multiStep(RestDocumentationContextProvider restDocumentation) { RequestSpecification spec = new RequestSpecBuilder().setPort(tomcat.getPort()) - .addFilter(documentationConfiguration(this.restDocumentation)) - .addFilter(document("{method-name}-{step}")).build(); + .addFilter(documentationConfiguration(restDocumentation)) + .addFilter(document("{method-name}-{step}")) + .build(); given(spec).get("/").then().statusCode(200); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-1/"), "http-request.adoc", + assertExpectedSnippetFilesExist(new File("build/generated-snippets/multi-step-1/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); given(spec).get("/").then().statusCode(200); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-2/"), "http-request.adoc", + assertExpectedSnippetFilesExist(new File("build/generated-snippets/multi-step-2/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); given(spec).get("/").then().statusCode(200); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-3/"), "http-request.adoc", + assertExpectedSnippetFilesExist(new File("build/generated-snippets/multi-step-3/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } @Test - public void additionalSnippets() throws Exception { + void additionalSnippets(RestDocumentationContextProvider restDocumentation) { RestDocumentationFilter documentation = document("{method-name}-{step}"); RequestSpecification spec = new RequestSpecBuilder().setPort(tomcat.getPort()) - .addFilter(documentationConfiguration(this.restDocumentation)) - .addFilter(documentation).build(); + .addFilter(documentationConfiguration(restDocumentation)) + .addFilter(documentation) + .build(); given(spec) - .filter(documentation - .document(responseHeaders(headerWithName("a").description("one"), - headerWithName("Foo").description("two")))) - .get("/").then().statusCode(200); - assertExpectedSnippetFilesExist( - new File("build/generated-snippets/additional-snippets-1/"), - "http-request.adoc", "http-response.adoc", "curl-request.adoc", - "response-headers.adoc"); + .filter(documentation.document( + responseHeaders(headerWithName("a").description("one"), headerWithName("Foo").description("two")))) + .get("/") + .then() + .statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/additional-snippets-1/"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", "response-headers.adoc"); } @Test - public void preprocessedRequest() throws Exception { + void responseWithCookie(RestDocumentationContextProvider restDocumentation) { + given().port(tomcat.getPort()) + .filter(documentationConfiguration(restDocumentation)) + .filter(document("set-cookie", + preprocessResponse(modifyHeaders().remove(HttpHeaders.DATE).remove(HttpHeaders.CONTENT_TYPE)))) + .get("/set-cookie") + .then() + .statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/set-cookie"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + assertThat(new File("build/generated-snippets/set-cookie/http-response.adoc")) + .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) + .header(HttpHeaders.SET_COOKIE, "name=value; Domain=localhost; HttpOnly") + .header("Keep-Alive", "timeout=60") + .header("Connection", "keep-alive"))); + } + + @Test + void preprocessedRequest(RestDocumentationContextProvider restDocumentation) { Pattern pattern = Pattern.compile("(\"alpha\")"); given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) - .header("a", "alpha").header("b", "bravo").contentType("application/json") - .accept("application/json").content("{\"a\":\"alpha\"}") - .filter(document("original-request")) - .filter(document("preprocessed-request", - preprocessRequest(prettyPrint(), - replacePattern(pattern, "\"<>\""), - modifyUris().removePort(), - removeHeaders("a", HttpHeaders.CONTENT_LENGTH)))) - .get("/").then().statusCode(200); - assertThat( - new File("build/generated-snippets/original-request/http-request.adoc"), - is(snippet(asciidoctor()) - .withContents(httpRequest(asciidoctor(), RequestMethod.GET, "/") - .header("a", "alpha").header("b", "bravo") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .header("Content-Type", "application/json; charset=UTF-8") - .header("Host", "localhost:" + tomcat.getPort()) - .header("Content-Length", "13") - .content("{\"a\":\"alpha\"}")))); + .filter(documentationConfiguration(restDocumentation)) + .header("a", "alpha") + .header("b", "bravo") + .contentType("application/json") + .accept("application/json") + .body("{\"a\":\"alpha\"}") + .filter(document("original-request")) + .filter(document("preprocessed-request", + preprocessRequest(prettyPrint(), replacePattern(pattern, "\"<>\""), modifyUris().removePort(), + modifyHeaders().remove("a").remove(HttpHeaders.CONTENT_LENGTH)))) + .get("/") + .then() + .statusCode(200); + assertThat(new File("build/generated-snippets/original-request/http-request.adoc")) + .has(content(httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/").header("a", "alpha") + .header("b", "bravo") + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .header("Content-Type", "application/json") + .header("Host", "localhost:" + tomcat.getPort()) + .header("Content-Length", "13") + .content("{\"a\":\"alpha\"}"))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); - assertThat( - new File( - "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet(asciidoctor()) - .withContents(httpRequest(asciidoctor(), RequestMethod.GET, "/") - .header("b", "bravo") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .header("Content-Type", "application/json; charset=UTF-8") - .header("Host", "localhost").content(prettyPrinted)))); + assertThat(new File("build/generated-snippets/preprocessed-request/http-request.adoc")) + .has(content(httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/").header("b", "bravo") + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .header("Content-Type", "application/json") + .header("Host", "localhost") + .content(prettyPrinted))); + } + + @Test + void defaultPreprocessedRequest(RestDocumentationContextProvider restDocumentation) { + Pattern pattern = Pattern.compile("(\"alpha\")"); + given().port(tomcat.getPort()) + .filter(documentationConfiguration(restDocumentation).operationPreprocessors() + .withRequestDefaults(prettyPrint(), replacePattern(pattern, "\"<>\""), modifyUris().removePort(), + modifyHeaders().remove("a").remove(HttpHeaders.CONTENT_LENGTH))) + .header("a", "alpha") + .header("b", "bravo") + .contentType("application/json") + .accept("application/json") + .body("{\"a\":\"alpha\"}") + .filter(document("default-preprocessed-request")) + .get("/") + .then() + .statusCode(200); + String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); + assertThat(new File("build/generated-snippets/default-preprocessed-request/http-request.adoc")) + .has(content(httpRequest(TemplateFormats.asciidoctor(), RequestMethod.GET, "/").header("b", "bravo") + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .header("Content-Type", "application/json") + .header("Host", "localhost") + .content(prettyPrinted))); } @Test - public void preprocessedResponse() throws Exception { + void preprocessedResponse(RestDocumentationContextProvider restDocumentation) { Pattern pattern = Pattern.compile("(\"alpha\")"); given().port(tomcat.getPort()) - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("original-response")) - .filter(document("preprocessed-response", preprocessResponse( - prettyPrint(), maskLinks(), - removeHeaders("a", "Transfer-Encoding", "Date", "Server"), - replacePattern(pattern, "\"<>\""), modifyUris() - .scheme("https").host("api.example.com").removePort()))) - .get("/").then().statusCode(200); + .filter(documentationConfiguration(restDocumentation)) + .filter(document("original-response")) + .filter(document("preprocessed-response", + preprocessResponse(prettyPrint(), maskLinks(), + modifyHeaders().remove("a").remove("Transfer-Encoding").remove("Date").remove("Server"), + replacePattern(pattern, "\"<>\""), + modifyUris().scheme("https").host("api.example.com").removePort()))) + .get("/") + .then() + .statusCode(200); String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); - assertThat( - new File( - "build/generated-snippets/preprocessed-response/http-response.adoc"), - is(snippet(asciidoctor()) - .withContents(httpResponse(asciidoctor(), HttpStatus.OK) - .header("Foo", "https://api.example.com/foo/bar") - .header("Content-Type", "application/json;charset=UTF-8") - .header(HttpHeaders.CONTENT_LENGTH, - prettyPrinted.getBytes().length) - .content(prettyPrinted)))); + assertThat(new File("build/generated-snippets/preprocessed-response/http-response.adoc")) + .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) + .header("Foo", "https://api.example.com/foo/bar") + .header("Content-Type", "application/json;charset=UTF-8") + .header("Keep-Alive", "timeout=60") + .header("Connection", "keep-alive") + .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length) + .content(prettyPrinted))); + } + + @Test + void defaultPreprocessedResponse(RestDocumentationContextProvider restDocumentation) { + Pattern pattern = Pattern.compile("(\"alpha\")"); + given().port(tomcat.getPort()) + .filter(documentationConfiguration(restDocumentation).operationPreprocessors() + .withResponseDefaults(prettyPrint(), maskLinks(), + modifyHeaders().remove("a").remove("Transfer-Encoding").remove("Date").remove("Server"), + replacePattern(pattern, "\"<>\""), + modifyUris().scheme("https").host("api.example.com").removePort())) + .filter(document("default-preprocessed-response")) + .get("/") + .then() + .statusCode(200); + String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); + assertThat(new File("build/generated-snippets/default-preprocessed-response/http-response.adoc")) + .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK) + .header("Foo", "https://api.example.com/foo/bar") + .header("Content-Type", "application/json;charset=UTF-8") + .header("Keep-Alive", "timeout=60") + .header("Connection", "keep-alive") + .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length) + .content(prettyPrinted))); } @Test - public void customSnippetTemplate() throws Exception { - ClassLoader classLoader = new URLClassLoader(new URL[] { - new File("src/test/resources/custom-snippet-templates").toURI().toURL() }, + void customSnippetTemplate(RestDocumentationContextProvider restDocumentation) throws MalformedURLException { + ClassLoader classLoader = new URLClassLoader( + new URL[] { new File("src/test/resources/custom-snippet-templates").toURI().toURL() }, getClass().getClassLoader()); ClassLoader previous = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(classLoader); try { - given().port(tomcat.getPort()).accept("application/json") - .filter(documentationConfiguration(this.restDocumentation)) - .filter(document("custom-snippet-template")).get("/").then() - .statusCode(200); + given().port(tomcat.getPort()) + .accept("application/json") + .filter(documentationConfiguration(restDocumentation)) + .filter(document("custom-snippet-template")) + .get("/") + .then() + .statusCode(200); } finally { Thread.currentThread().setContextClassLoader(previous); } - assertThat( - new File( - "build/generated-snippets/custom-snippet-template/curl-request.adoc"), - is(snippet(asciidoctor()).withContents(equalTo("Custom curl request")))); + assertThat(new File("build/generated-snippets/custom-snippet-template/curl-request.adoc")) + .hasContent("Custom curl request"); + } + + @Test + void exceptionShouldBeThrownWhenCallDocumentRequestSpecificationNotConfigured() { + assertThatThrownBy(() -> given().port(tomcat.getPort()).filter(document("default")).get("/")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("REST Docs configuration not found. Did you forget to add a " + + "RestAssuredRestDocumentationConfigurer as a filter when building the RequestSpecification?"); + } + + @Test + void exceptionShouldBeThrownWhenCallDocumentSnippetsRequestSpecificationNotConfigured() { + RestDocumentationFilter documentation = document("{method-name}-{step}"); + assertThatThrownBy(() -> given().port(tomcat.getPort()) + .filter(documentation.document(responseHeaders(headerWithName("a").description("one")))) + .get("/")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("REST Docs configuration not found. Did you forget to add a " + + "RestAssuredRestDocumentationConfigurer as a filter when building the " + + "RequestSpecification?"); } private void assertExpectedSnippetFilesExist(File directory, String... snippets) { for (String snippet : snippets) { - File snippetFile = new File(directory, snippet); - assertTrue("Snippet " + snippetFile + " not found", snippetFile.isFile()); + assertThat(new File(directory, snippet)).isFile(); } } + private Condition content(final Condition delegate) { + return new Condition<>() { + + @Override + public boolean matches(File value) { + try { + String copyToString = FileCopyUtils + .copyToString(new InputStreamReader(new FileInputStream(value), StandardCharsets.UTF_8)); + System.out.println(copyToString); + return delegate.matches(copyToString); + } + catch (IOException ex) { + fail("Failed to read '" + value + "'", ex); + return false; + } + } + + }; + } + + private CodeBlockCondition codeBlock(TemplateFormat format, String language) { + return SnippetConditions.codeBlock(format, language); + } + + private HttpRequestCondition httpRequest(TemplateFormat format, RequestMethod requestMethod, String uri) { + return SnippetConditions.httpRequest(format, requestMethod, uri); + } + + private HttpResponseCondition httpResponse(TemplateFormat format, HttpStatus status) { + return SnippetConditions.httpResponse(format, status); + } + } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java index 0490770c1..f91e1943c 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,51 +17,72 @@ package org.springframework.restdocs.restassured; import java.io.IOException; +import java.io.InputStreamReader; import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; -import org.junit.rules.ExternalResource; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; + +import org.springframework.http.MediaType; +import org.springframework.util.FileCopyUtils; /** - * {@link ExternalResource} that starts and stops a Tomcat server. + * {@link Extension} that starts and stops a Tomcat server. * * @author Andy Wilkinson */ -class TomcatServer extends ExternalResource { - - private Tomcat tomcat; +class TomcatServer implements BeforeAllCallback, AfterAllCallback { private int port; @Override - protected void before() throws LifecycleException { - this.tomcat = new Tomcat(); - this.tomcat.getConnector().setPort(0); - Context context = this.tomcat.addContext("/", null); - this.tomcat.addServlet("/", "test", new TestServlet()); - context.addServletMappingDecoded("/", "test"); - this.tomcat.start(); - this.port = this.tomcat.getConnector().getLocalPort(); + public void beforeAll(ExtensionContext extensionContext) { + Store store = extensionContext.getStore(Namespace.create(TomcatServer.class)); + store.getOrComputeIfAbsent(Tomcat.class, (key) -> { + Tomcat tomcat = new Tomcat(); + tomcat.getConnector().setPort(0); + Context context = tomcat.addContext("/", null); + tomcat.addServlet("/", "test", new TestServlet()); + context.addServletMappingDecoded("/", "test"); + tomcat.addServlet("/", "set-cookie", new CookiesServlet()); + context.addServletMappingDecoded("/set-cookie", "set-cookie"); + tomcat.addServlet("/", "query-parameter", new QueryParameterServlet()); + context.addServletMappingDecoded("/query-parameter", "query-parameter"); + tomcat.addServlet("/", "form-url-encoded", new FormUrlEncodedServlet()); + context.addServletMappingDecoded("/form-url-encoded", "form-url-encoded"); + try { + tomcat.start(); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + this.port = tomcat.getConnector().getLocalPort(); + return tomcat; + }); } @Override - protected void after() { - try { - this.tomcat.stop(); - } - catch (LifecycleException ex) { - throw new RuntimeException(ex); + public void afterAll(ExtensionContext extensionContext) throws LifecycleException { + Store store = extensionContext.getStore(Namespace.create(TomcatServer.class)); + Tomcat tomcat = store.get(Tomcat.class, Tomcat.class); + if (tomcat != null) { + tomcat.stop(); } } @@ -86,8 +107,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) respondWithJson(response); } - private void respondWithJson(HttpServletResponse response) - throws IOException, JsonProcessingException { + private void respondWithJson(HttpServletResponse response) throws IOException, JsonProcessingException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); Map content = new HashMap<>(); @@ -104,4 +124,49 @@ private void respondWithJson(HttpServletResponse response) } + /** + * {@link HttpServlet} used to handle cookies-related requests in the tests. + */ + private static final class CookiesServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + Cookie cookie = new Cookie("name", "value"); + cookie.setDomain("localhost"); + cookie.setHttpOnly(true); + + resp.addCookie(cookie); + } + + } + + private static final class QueryParameterServlet extends HttpServlet { + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (!req.getQueryString().equals("a=alpha&a=apple&b=bravo")) { + throw new ServletException("Incorrect query string"); + } + resp.setStatus(200); + } + + } + + private static final class FormUrlEncodedServlet extends HttpServlet { + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (!MediaType.APPLICATION_FORM_URLENCODED + .isCompatibleWith(MediaType.parseMediaType(req.getContentType()))) { + throw new ServletException("Incorrect Content-Type"); + } + String content = FileCopyUtils.copyToString(new InputStreamReader(req.getInputStream())); + if (!"a=alpha&a=apple&b=bravo".equals(content)) { + throw new ServletException("Incorrect body content"); + } + resp.setStatus(200); + } + + } + } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java deleted file mode 100644 index 746e2915d..000000000 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java +++ /dev/null @@ -1,373 +0,0 @@ -/* - * Copyright 2014-2019 the original author or 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. - */ - -package org.springframework.restdocs.restassured.operation.preprocess; - -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Test; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.OperationRequestPartFactory; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.Parameters; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -/** - * Tests for {@link UriModifyingOperationPreprocessor}. - * - * @author Andy Wilkinson - */ -public class UriModifyingOperationPreprocessorTests { - - private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - - private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - - private final UriModifyingOperationPreprocessor preprocessor = new UriModifyingOperationPreprocessor(); - - @Test - public void requestUriSchemeCanBeModified() { - this.preprocessor.scheme("https"); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("http://localhost:12345")); - assertThat(processed.getUri(), - is(equalTo(URI.create("https://localhost:12345")))); - } - - @Test - public void requestUriHostCanBeModified() { - this.preprocessor.host("api.example.com"); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("https://api.foo.com:12345")); - assertThat(processed.getUri(), - is(equalTo(URI.create("https://api.example.com:12345")))); - assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST), - is(equalTo("api.example.com:12345"))); - } - - @Test - public void requestUriPortCanBeModified() { - this.preprocessor.port(23456); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("https://api.example.com:12345")); - assertThat(processed.getUri(), - is(equalTo(URI.create("https://api.example.com:23456")))); - assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST), - is(equalTo("api.example.com:23456"))); - } - - @Test - public void requestUriPortCanBeRemoved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("https://api.example.com:12345")); - assertThat(processed.getUri(), is(equalTo(URI.create("https://api.example.com")))); - assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST), - is(equalTo("api.example.com"))); - } - - @Test - public void requestUriPathIsPreserved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("https://api.example.com:12345/foo/bar")); - assertThat(processed.getUri(), - is(equalTo(URI.create("https://api.example.com/foo/bar")))); - } - - @Test - public void requestUriQueryIsPreserved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("https://api.example.com:12345?foo=bar")); - assertThat(processed.getUri(), - is(equalTo(URI.create("https://api.example.com?foo=bar")))); - } - - @Test - public void requestUriAnchorIsPreserved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("https://api.example.com:12345#foo")); - assertThat(processed.getUri(), - is(equalTo(URI.create("https://api.example.com#foo")))); - } - - @Test - public void requestContentUriSchemeCanBeModified() { - this.preprocessor.scheme("https"); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent( - "The uri 'http://localhost:12345' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'https://localhost:12345' should be used"))); - } - - @Test - public void requestContentUriHostCanBeModified() { - this.preprocessor.host("api.example.com"); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent( - "The uri 'https://localhost:12345' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'https://api.example.com:12345' should be used"))); - } - - @Test - public void requestContentUriPortCanBeModified() { - this.preprocessor.port(23456); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent( - "The uri 'http://localhost:12345' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'http://localhost:23456' should be used"))); - } - - @Test - public void requestContentUriPortCanBeRemoved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent( - "The uri 'http://localhost:12345' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'http://localhost' should be used"))); - } - - @Test - public void multipleRequestContentUrisCanBeModified() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent( - "Use 'http://localhost:12345' or 'https://localhost:23456' to access the service")); - assertThat(new String(processed.getContent()), is(equalTo( - "Use 'http://localhost' or 'https://localhost' to access the service"))); - } - - @Test - public void requestContentUriPathIsPreserved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent( - "The uri 'http://localhost:12345/foo/bar' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'http://localhost/foo/bar' should be used"))); - } - - @Test - public void requestContentUriQueryIsPreserved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent( - "The uri 'http://localhost:12345?foo=bar' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'http://localhost?foo=bar' should be used"))); - } - - @Test - public void requestContentUriAnchorIsPreserved() { - this.preprocessor.removePort(); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithContent( - "The uri 'http://localhost:12345#foo' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'http://localhost#foo' should be used"))); - } - - @Test - public void responseContentUriSchemeCanBeModified() { - this.preprocessor.scheme("https"); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent( - "The uri 'http://localhost:12345' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'https://localhost:12345' should be used"))); - } - - @Test - public void responseContentUriHostCanBeModified() { - this.preprocessor.host("api.example.com"); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent( - "The uri 'https://localhost:12345' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'https://api.example.com:12345' should be used"))); - } - - @Test - public void responseContentUriPortCanBeModified() { - this.preprocessor.port(23456); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent( - "The uri 'http://localhost:12345' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'http://localhost:23456' should be used"))); - } - - @Test - public void responseContentUriPortCanBeRemoved() { - this.preprocessor.removePort(); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent( - "The uri 'http://localhost:12345' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'http://localhost' should be used"))); - } - - @Test - public void multipleResponseContentUrisCanBeModified() { - this.preprocessor.removePort(); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent( - "Use 'http://localhost:12345' or 'https://localhost:23456' to access the service")); - assertThat(new String(processed.getContent()), is(equalTo( - "Use 'http://localhost' or 'https://localhost' to access the service"))); - } - - @Test - public void responseContentUriPathIsPreserved() { - this.preprocessor.removePort(); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent( - "The uri 'http://localhost:12345/foo/bar' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'http://localhost/foo/bar' should be used"))); - } - - @Test - public void responseContentUriQueryIsPreserved() { - this.preprocessor.removePort(); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent( - "The uri 'http://localhost:12345?foo=bar' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'http://localhost?foo=bar' should be used"))); - } - - @Test - public void responseContentUriAnchorIsPreserved() { - this.preprocessor.removePort(); - OperationResponse processed = this.preprocessor - .preprocess(createResponseWithContent( - "The uri 'http://localhost:12345#foo' should be used")); - assertThat(new String(processed.getContent()), - is(equalTo("The uri 'http://localhost#foo' should be used"))); - } - - @Test - public void urisInRequestHeadersCanBeModified() { - OperationRequest processed = this.preprocessor.host("api.example.com") - .preprocess(createRequestWithHeader("Foo", "https://locahost:12345")); - assertThat(processed.getHeaders().getFirst("Foo"), - is(equalTo("https://api.example.com:12345"))); - assertThat(processed.getHeaders().getFirst("Host"), - is(equalTo("api.example.com"))); - } - - @Test - public void urisInResponseHeadersCanBeModified() { - OperationResponse processed = this.preprocessor.host("api.example.com") - .preprocess(createResponseWithHeader("Foo", "https://locahost:12345")); - assertThat(processed.getHeaders().getFirst("Foo"), - is(equalTo("https://api.example.com:12345"))); - } - - @Test - public void urisInRequestPartHeadersCanBeModified() { - OperationRequest processed = this.preprocessor.host("api.example.com").preprocess( - createRequestWithPartWithHeader("Foo", "https://locahost:12345")); - assertThat(processed.getParts().iterator().next().getHeaders().getFirst("Foo"), - is(equalTo("https://api.example.com:12345"))); - } - - @Test - public void urisInRequestPartContentCanBeModified() { - OperationRequest processed = this.preprocessor.host("api.example.com") - .preprocess(createRequestWithPartWithContent( - "The uri 'https://localhost:12345' should be used")); - assertThat(new String(processed.getParts().iterator().next().getContent()), - is(equalTo("The uri 'https://api.example.com:12345' should be used"))); - } - - @Test - public void modifiedUriDoesNotGetDoubleEncoded() { - this.preprocessor.scheme("https"); - OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("http://localhost:12345?foo=%7B%7D")); - assertThat(processed.getUri(), - is(equalTo(URI.create("https://localhost:12345?foo=%7B%7D")))); - - } - - private OperationRequest createRequestWithUri(String uri) { - return this.requestFactory.create(URI.create(uri), HttpMethod.GET, new byte[0], - new HttpHeaders(), new Parameters(), - Collections.emptyList()); - } - - private OperationRequest createRequestWithContent(String content) { - return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, - content.getBytes(), new HttpHeaders(), new Parameters(), - Collections.emptyList()); - } - - private OperationRequest createRequestWithHeader(String name, String value) { - HttpHeaders headers = new HttpHeaders(); - headers.add(name, value); - return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, - new byte[0], headers, new Parameters(), - Collections.emptyList()); - } - - private OperationRequest createRequestWithPartWithHeader(String name, String value) { - HttpHeaders headers = new HttpHeaders(); - headers.add(name, value); - return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, - new byte[0], new HttpHeaders(), new Parameters(), - Arrays.asList(new OperationRequestPartFactory().create("part", "fileName", - new byte[0], headers))); - } - - private OperationRequest createRequestWithPartWithContent(String content) { - return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, - new byte[0], new HttpHeaders(), new Parameters(), - Arrays.asList(new OperationRequestPartFactory().create("part", "fileName", - content.getBytes(), new HttpHeaders()))); - } - - private OperationResponse createResponseWithContent(String content) { - return this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), - content.getBytes()); - } - - private OperationResponse createResponseWithHeader(String name, String value) { - HttpHeaders headers = new HttpHeaders(); - headers.add(name, value); - return this.responseFactory.create(HttpStatus.OK, headers, new byte[0]); - } - -} diff --git a/spring-restdocs-webtestclient/build.gradle b/spring-restdocs-webtestclient/build.gradle new file mode 100644 index 000000000..7675c0d6c --- /dev/null +++ b/spring-restdocs-webtestclient/build.gradle @@ -0,0 +1,26 @@ +plugins { + id "java-library" + id "maven-publish" +} + +description = "Spring REST Docs WebFlux" + +dependencies { + api(project(":spring-restdocs-core")) + api("org.springframework:spring-test") + api("org.springframework:spring-webflux") + + compileOnly("org.hamcrest:hamcrest-core") + + internal(platform(project(":spring-restdocs-platform"))) + + testCompileOnly("org.hamcrest:hamcrest-core") + + testImplementation(testFixtures(project(":spring-restdocs-core"))) + + testRuntimeOnly("org.springframework:spring-context") +} + +tasks.named("test") { + useJUnitPlatform(); +} diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientOperationPreprocessorsConfigurer.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientOperationPreprocessorsConfigurer.java new file mode 100644 index 000000000..6c612acf7 --- /dev/null +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientOperationPreprocessorsConfigurer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2019 the original author or 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. + */ + +package org.springframework.restdocs.webtestclient; + +import reactor.core.publisher.Mono; + +import org.springframework.restdocs.config.OperationPreprocessorsConfigurer; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; + +/** + * A configurer that can be used to configure the operation preprocessors. + * + * @author Andy Wilkinson + * @since 2.0.0 + */ +public final class WebTestClientOperationPreprocessorsConfigurer extends + OperationPreprocessorsConfigurer + implements ExchangeFilterFunction { + + WebTestClientOperationPreprocessorsConfigurer(WebTestClientRestDocumentationConfigurer parent) { + super(parent); + } + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + return and().filter(request, next); + } + +} diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java new file mode 100644 index 000000000..7fef86918 --- /dev/null +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java @@ -0,0 +1,134 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package org.springframework.restdocs.webtestclient; + +import java.io.ByteArrayOutputStream; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.DefaultDataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; +import org.springframework.http.codec.multipart.FilePart; +import org.springframework.http.codec.multipart.MultipartHttpMessageReader; +import org.springframework.http.codec.multipart.Part; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; +import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.restdocs.operation.RequestCookie; +import org.springframework.test.web.reactive.server.ExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.LinkedMultiValueMap; + +/** + * A {@link RequestConverter} for creating an {@link OperationRequest} derived from an + * {@link ExchangeResult}. + * + * @author Andy Wilkinson + */ +class WebTestClientRequestConverter implements RequestConverter { + + @Override + public OperationRequest convert(ExchangeResult result) { + HttpHeaders headers = extractRequestHeaders(result); + return new OperationRequestFactory().create(result.getUrl(), result.getMethod(), result.getRequestBodyContent(), + headers, extractRequestParts(result), extractCookies(headers)); + } + + private HttpHeaders extractRequestHeaders(ExchangeResult result) { + HttpHeaders extracted = new HttpHeaders(); + extracted.putAll(result.getRequestHeaders()); + extracted.remove(WebTestClient.WEBTESTCLIENT_REQUEST_ID); + return extracted; + } + + private List extractRequestParts(ExchangeResult result) { + HttpMessageReader partHttpMessageReader = new DefaultPartHttpMessageReader(); + return new MultipartHttpMessageReader(partHttpMessageReader) + .readMono(ResolvableType.forClass(Part.class), new ExchangeResultReactiveHttpInputMessage(result), + Collections.emptyMap()) + .onErrorReturn(new LinkedMultiValueMap<>()) + .block() + .values() + .stream() + .flatMap((parts) -> parts.stream().map(this::createOperationRequestPart)) + .collect(Collectors.toList()); + } + + private OperationRequestPart createOperationRequestPart(Part part) { + ByteArrayOutputStream content = readPartBodyContent(part); + return new OperationRequestPartFactory().create(part.name(), + (part instanceof FilePart) ? ((FilePart) part).filename() : null, content.toByteArray(), + part.headers()); + } + + private ByteArrayOutputStream readPartBodyContent(Part part) { + ByteArrayOutputStream contentStream = new ByteArrayOutputStream(); + DataBufferUtils.write(part.content(), contentStream).blockFirst(); + return contentStream; + } + + private Collection extractCookies(HttpHeaders headers) { + List cookieHeaders = headers.get(HttpHeaders.COOKIE); + if (cookieHeaders == null) { + return Collections.emptyList(); + } + headers.remove(HttpHeaders.COOKIE); + return cookieHeaders.stream().map(this::createRequestCookie).collect(Collectors.toList()); + } + + private RequestCookie createRequestCookie(String header) { + String[] components = header.split("="); + return new RequestCookie(components[0], components[1]); + } + + private final class ExchangeResultReactiveHttpInputMessage implements ReactiveHttpInputMessage { + + private final ExchangeResult result; + + private ExchangeResultReactiveHttpInputMessage(ExchangeResult result) { + this.result = result; + } + + @Override + public HttpHeaders getHeaders() { + return this.result.getRequestHeaders(); + } + + @Override + public Flux getBody() { + byte[] requestBodyContent = this.result.getRequestBodyContent(); + DefaultDataBuffer buffer = new DefaultDataBufferFactory().allocateBuffer(requestBodyContent.length); + buffer.write(requestBodyContent); + return Flux.fromArray(new DataBuffer[] { buffer }); + } + + } + +} diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverter.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverter.java new file mode 100644 index 000000000..5a8416bd4 --- /dev/null +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverter.java @@ -0,0 +1,107 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.webtestclient; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.operation.ResponseCookie; +import org.springframework.test.web.reactive.server.ExchangeResult; +import org.springframework.util.StringUtils; + +/** + * A {@link ResponseConverter} for creating an {@link OperationResponse} derived from an + * {@link ExchangeResult}. + * + * @author Andy Wilkinson + * @author Clyde Stubbs + */ +class WebTestClientResponseConverter implements ResponseConverter { + + @Override + public OperationResponse convert(ExchangeResult result) { + Collection cookies = extractCookies(result); + return new OperationResponseFactory().create(result.getStatus(), extractHeaders(result), + result.getResponseBodyContent(), cookies); + } + + private HttpHeaders extractHeaders(ExchangeResult result) { + HttpHeaders headers = result.getResponseHeaders(); + if (result.getResponseCookies().isEmpty() || headers.containsHeader(HttpHeaders.SET_COOKIE)) { + return headers; + } + result.getResponseCookies() + .values() + .stream() + .flatMap(Collection::stream) + .forEach((cookie) -> headers.add(HttpHeaders.SET_COOKIE, generateSetCookieHeader(cookie))); + return headers; + } + + private String generateSetCookieHeader(org.springframework.http.ResponseCookie cookie) { + StringBuilder header = new StringBuilder(); + header.append(cookie.getName()); + header.append('='); + appendIfAvailable(header, cookie.getValue()); + long maxAge = cookie.getMaxAge().getSeconds(); + if (maxAge > -1) { + header.append("; Max-Age="); + header.append(maxAge); + } + appendIfAvailable(header, "; Domain=", cookie.getDomain()); + appendIfAvailable(header, "; Path=", cookie.getPath()); + if (cookie.isSecure()) { + header.append("; Secure"); + } + if (cookie.isHttpOnly()) { + header.append("; HttpOnly"); + } + return header.toString(); + } + + private Collection extractCookies(ExchangeResult result) { + return result.getResponseCookies() + .values() + .stream() + .flatMap(List::stream) + .map(this::createResponseCookie) + .collect(Collectors.toSet()); + } + + private void appendIfAvailable(StringBuilder header, String value) { + if (StringUtils.hasText(value)) { + header.append(value); + } + } + + private ResponseCookie createResponseCookie(org.springframework.http.ResponseCookie original) { + return new ResponseCookie(original.getName(), original.getValue()); + } + + private void appendIfAvailable(StringBuilder header, String name, String value) { + if (StringUtils.hasText(value)) { + header.append(name); + header.append(value); + } + } + +} diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentation.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentation.java new file mode 100644 index 000000000..a3f48324b --- /dev/null +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentation.java @@ -0,0 +1,144 @@ +/* + * Copyright 2014-2023 the original author or 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. + */ + +package org.springframework.restdocs.webtestclient; + +import java.util.Map; +import java.util.function.Consumer; + +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.generate.RestDocumentationGenerator; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.test.web.reactive.server.ExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.reactive.server.WebTestClient.BodyContentSpec; +import org.springframework.test.web.reactive.server.WebTestClient.BodySpec; +import org.springframework.test.web.reactive.server.WebTestClient.Builder; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; + +/** + * Static factory methods for documenting RESTful APIs using WebFlux's + * {@link WebTestClient}. + * + * @author Andy Wilkinson + * @since 2.0.0 + */ +public abstract class WebTestClientRestDocumentation { + + private static final WebTestClientRequestConverter REQUEST_CONVERTER = new WebTestClientRequestConverter(); + + private static final WebTestClientResponseConverter RESPONSE_CONVERTER = new WebTestClientResponseConverter(); + + private WebTestClientRestDocumentation() { + + } + + /** + * Provides access to a {@link ExchangeFilterFunction} that can be used to configure a + * {@link WebTestClient} instance using the given {@code contextProvider}. + * @param contextProvider the context provider + * @return the configurer + * @see Builder#filter(ExchangeFilterFunction) + */ + public static WebTestClientRestDocumentationConfigurer documentationConfiguration( + RestDocumentationContextProvider contextProvider) { + return new WebTestClientRestDocumentationConfigurer(contextProvider); + } + + /** + * Returns a {@link Consumer} that, when called, documents the API call with the given + * {@code identifier} using the given {@code snippets} in addition to any default + * snippets. + * @param identifier an identifier for the API call that is being documented + * @param snippets the snippets + * @param the type of {@link ExchangeResult} that will be consumed + * @return the {@link Consumer} that will document the API call represented by the + * {@link ExchangeResult}. + * @see BodySpec#consumeWith(Consumer) + * @see BodyContentSpec#consumeWith(Consumer) + */ + public static Consumer document(String identifier, Snippet... snippets) { + return (result) -> new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets) + .handle(result, result, retrieveConfiguration(result)); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets} in addition to any default snippets. The given + * {@code requestPreprocessor} is applied to the request before it is documented. + * @param identifier an identifier for the API call that is being documented + * @param requestPreprocessor the request preprocessor + * @param snippets the snippets + * @param the type of {@link ExchangeResult} that will be consumed + * @return the {@link Consumer} that will document the API call represented by the + * {@link ExchangeResult}. + */ + public static Consumer document(String identifier, + OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { + return (result) -> new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, + requestPreprocessor, snippets) + .handle(result, result, retrieveConfiguration(result)); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets} in addition to any default snippets. The given + * {@code responsePreprocessor} is applied to the request before it is documented. + * @param identifier an identifier for the API call that is being documented + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets + * @param the type of {@link ExchangeResult} that will be consumed + * @return the {@link Consumer} that will document the API call represented by the + * {@link ExchangeResult}. + */ + public static Consumer document(String identifier, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + return (result) -> new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, + responsePreprocessor, snippets) + .handle(result, result, retrieveConfiguration(result)); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets} in addition to any default snippets. The given + * {@code requestPreprocessor} and {@code responsePreprocessor} are applied to the + * request and response respectively before they are documented. + * @param identifier an identifier for the API call that is being documented + * @param requestPreprocessor the request preprocessor + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets + * @param the type of {@link ExchangeResult} that will be consumed + * @return the {@link Consumer} that will document the API call represented by the + * {@link ExchangeResult}. + */ + public static Consumer document(String identifier, + OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor, + Snippet... snippets) { + return (result) -> new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, + requestPreprocessor, responsePreprocessor, snippets) + .handle(result, result, retrieveConfiguration(result)); + } + + private static Map retrieveConfiguration(ExchangeResult result) { + Map configuration = WebTestClientRestDocumentationConfigurer + .retrieveConfiguration(result.getRequestHeaders()); + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, result.getUriTemplate()); + return configuration; + } + +} diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurer.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurer.java new file mode 100644 index 000000000..fb3f4d507 --- /dev/null +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurer.java @@ -0,0 +1,110 @@ +/* + * Copyright 2014-2019 the original author or 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. + */ + +package org.springframework.restdocs.webtestclient; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.config.RestDocumentationConfigurer; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; + +/** + * A WebFlux-specific {@link RestDocumentationConfigurer}. + * + * @author Andy Wilkinson + * @since 2.0.0 + */ +public class WebTestClientRestDocumentationConfigurer extends + RestDocumentationConfigurer + implements ExchangeFilterFunction { + + private final WebTestClientSnippetConfigurer snippetConfigurer = new WebTestClientSnippetConfigurer(this); + + private static final Map> configurations = new ConcurrentHashMap<>(); + + private final WebTestClientOperationPreprocessorsConfigurer operationPreprocessorsConfigurer = new WebTestClientOperationPreprocessorsConfigurer( + this); + + private final RestDocumentationContextProvider contextProvider; + + WebTestClientRestDocumentationConfigurer(RestDocumentationContextProvider contextProvider) { + this.contextProvider = contextProvider; + } + + @Override + public WebTestClientSnippetConfigurer snippets() { + return this.snippetConfigurer; + } + + @Override + public WebTestClientOperationPreprocessorsConfigurer operationPreprocessors() { + return this.operationPreprocessorsConfigurer; + } + + private Map createConfiguration() { + RestDocumentationContext context = this.contextProvider.beforeOperation(); + Map configuration = new HashMap<>(); + configuration.put(RestDocumentationContext.class.getName(), context); + apply(configuration, context); + return configuration; + } + + static Map retrieveConfiguration(HttpHeaders headers) { + String requestId = headers.getFirst(WebTestClient.WEBTESTCLIENT_REQUEST_ID); + Map configuration = configurations.remove(requestId); + Assert.state(configuration != null, () -> "REST Docs configuration not found. Did you forget to register a " + + WebTestClientRestDocumentationConfigurer.class.getSimpleName() + " as a filter?"); + return configuration; + } + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + String index = request.headers().getFirst(WebTestClient.WEBTESTCLIENT_REQUEST_ID); + configurations.put(index, createConfiguration()); + return next.exchange(applyUriDefaults(request)); + } + + private ClientRequest applyUriDefaults(ClientRequest request) { + URI requestUri = request.url(); + if (StringUtils.hasLength(requestUri.getHost())) { + return request; + } + try { + requestUri = new URI("http", requestUri.getUserInfo(), "localhost", 8080, requestUri.getPath(), + requestUri.getQuery(), requestUri.getFragment()); + return ClientRequest.from(request).url(requestUri).build(); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientSnippetConfigurer.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientSnippetConfigurer.java new file mode 100644 index 000000000..68c0916c6 --- /dev/null +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientSnippetConfigurer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014-2019 the original author or 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. + */ + +package org.springframework.restdocs.webtestclient; + +import reactor.core.publisher.Mono; + +import org.springframework.restdocs.config.SnippetConfigurer; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; + +/** + * A {@link SnippetConfigurer} for WebFlux that can be used to configure the generated + * documentation snippets. + * + * @author Andy Wilkinson + * @since 2.0.0 + */ +public class WebTestClientSnippetConfigurer + extends SnippetConfigurer + implements ExchangeFilterFunction { + + WebTestClientSnippetConfigurer(WebTestClientRestDocumentationConfigurer parent) { + super(parent); + } + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + return and().filter(request, next); + } + +} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/TagRepository.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/package-info.java similarity index 73% rename from samples/rest-notes-slate/src/main/java/com/example/notes/TagRepository.java rename to spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/package-info.java index 96ad37917..9a49f263e 100644 --- a/samples/rest-notes-slate/src/main/java/com/example/notes/TagRepository.java +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,7 @@ * limitations under the License. */ -package com.example.notes; - -import org.springframework.data.repository.CrudRepository; - -public interface TagRepository extends CrudRepository { - -} +/** + * Core classes for using Spring REST Docs with Spring Framework's WebTestClient. + */ +package org.springframework.restdocs.webtestclient; diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java new file mode 100644 index 000000000..1087ea411 --- /dev/null +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java @@ -0,0 +1,339 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.webtestclient; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.test.web.reactive.server.ExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyExtractors; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.POST; + +/** + * Tests for {@link WebTestClientRequestConverter}. + * + * @author Andy Wilkinson + */ +class WebTestClientRequestConverterTests { + + private final WebTestClientRequestConverter converter = new WebTestClientRequestConverter(); + + @Test + void httpRequest() { + ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) + .configureClient() + .baseUrl("http://localhost") + .build() + .get() + .uri("/foo") + .exchange() + .expectBody() + .returnResult(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); + } + + @Test + void httpRequestWithCustomPort() { + ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) + .configureClient() + .baseUrl("http://localhost:8080") + .build() + .get() + .uri("/foo") + .exchange() + .expectBody() + .returnResult(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost:8080/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); + } + + @Test + void requestWithHeaders() { + ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/"), (req) -> null)) + .configureClient() + .baseUrl("http://localhost") + .build() + .get() + .uri("/foo") + .header("a", "alpha", "apple") + .header("b", "bravo") + .exchange() + .expectBody() + .returnResult(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); + assertThat(request.getHeaders().headerSet()).contains(entry("a", Arrays.asList("alpha", "apple")), + entry("b", Arrays.asList("bravo"))); + } + + @Test + void httpsRequest() { + ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) + .configureClient() + .baseUrl("https://localhost") + .build() + .get() + .uri("/foo") + .exchange() + .expectBody() + .returnResult(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("https://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); + } + + @Test + void httpsRequestWithCustomPort() { + ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) + .configureClient() + .baseUrl("https://localhost:8443") + .build() + .get() + .uri("/foo") + .exchange() + .expectBody() + .returnResult(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("https://localhost:8443/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); + } + + @Test + void getRequestWithQueryString() { + ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) + .configureClient() + .baseUrl("http://localhost") + .build() + .get() + .uri("/foo?a=alpha&b=bravo") + .exchange() + .expectBody() + .returnResult(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?a=alpha&b=bravo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); + } + + @Test + void postRequestWithFormDataParameters() { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.addAll("a", Arrays.asList("alpha", "apple")); + parameters.addAll("b", Arrays.asList("br&vo")); + ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> { + req.body(BodyExtractors.toFormData()).block(); + return null; + })) + .configureClient() + .baseUrl("http://localhost") + .build() + .post() + .uri("/foo") + .body(BodyInserters.fromFormData(parameters)) + .exchange() + .expectBody() + .returnResult(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(request.getContentAsString()).isEqualTo("a=alpha&a=apple&b=br%26vo"); + assertThat(request.getHeaders().getContentType()).satisfiesAnyOf( + (mediaType) -> assertThat(mediaType) + .isEqualTo(new MediaType(MediaType.APPLICATION_FORM_URLENCODED, StandardCharsets.UTF_8)), + (mediaType) -> assertThat(mediaType).isEqualTo(new MediaType(MediaType.APPLICATION_FORM_URLENCODED))); + } + + @Test + void postRequestWithQueryStringParameters() { + ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> { + req.body(BodyExtractors.toFormData()).block(); + return null; + })) + .configureClient() + .baseUrl("http://localhost") + .build() + .post() + .uri(URI.create("http://localhost/foo?a=alpha&a=apple&b=br%26vo")) + .exchange() + .expectBody() + .returnResult(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?a=alpha&a=apple&b=br%26vo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + } + + @Test + void postRequestWithQueryStringAndFormDataParameters() { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.addAll("a", Arrays.asList("apple")); + ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> { + req.body(BodyExtractors.toFormData()).block(); + return null; + })) + .configureClient() + .baseUrl("http://localhost") + .build() + .post() + .uri(URI.create("http://localhost/foo?a=alpha&b=br%26vo")) + .body(BodyInserters.fromFormData(parameters)) + .exchange() + .expectBody() + .returnResult(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?a=alpha&b=br%26vo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(request.getContentAsString()).isEqualTo("a=apple"); + assertThat(request.getHeaders().getContentType()).satisfiesAnyOf( + (mediaType) -> assertThat(mediaType) + .isEqualTo(new MediaType(MediaType.APPLICATION_FORM_URLENCODED, StandardCharsets.UTF_8)), + (mediaType) -> assertThat(mediaType).isEqualTo(new MediaType(MediaType.APPLICATION_FORM_URLENCODED))); + } + + @Test + void postRequestWithNoContentType() { + ExchangeResult result = WebTestClient + .bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> ServerResponse.ok().build())) + .configureClient() + .baseUrl("http://localhost") + .build() + .post() + .uri("/foo") + .exchange() + .expectBody() + .returnResult(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + } + + @Test + void multipartUpload() { + MultiValueMap multipartData = new LinkedMultiValueMap<>(); + multipartData.add("file", new byte[] { 1, 2, 3, 4 }); + ExchangeResult result = WebTestClient + .bindToRouterFunction(RouterFunctions.route(POST("/foo"), + (req) -> ServerResponse.ok() + .body(req.body(BodyExtractors.toMultipartData()).map((parts) -> parts.size()), Integer.class))) + .configureClient() + .baseUrl("http://localhost") + .build() + .post() + .uri("/foo") + .body(BodyInserters.fromMultipartData(multipartData)) + .exchange() + .expectBody() + .returnResult(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(request.getParts()).hasSize(1); + OperationRequestPart part = request.getParts().iterator().next(); + assertThat(part.getName()).isEqualTo("file"); + assertThat(part.getSubmittedFileName()).isNull(); + assertThat(part.getHeaders().size()).isEqualTo(2); + assertThat(part.getHeaders().getContentLength()).isEqualTo(4L); + assertThat(part.getHeaders().getContentDisposition().getName()).isEqualTo("file"); + assertThat(part.getContent()).containsExactly(1, 2, 3, 4); + } + + @Test + void multipartUploadFromResource() { + MultiValueMap multipartData = new LinkedMultiValueMap<>(); + multipartData.add("file", new ByteArrayResource(new byte[] { 1, 2, 3, 4 }) { + + @Override + public String getFilename() { + return "image.png"; + } + + }); + ExchangeResult result = WebTestClient + .bindToRouterFunction(RouterFunctions.route(POST("/foo"), + (req) -> ServerResponse.ok() + .body(req.body(BodyExtractors.toMultipartData()).map((parts) -> parts.size()), Integer.class))) + .configureClient() + .baseUrl("http://localhost") + .build() + .post() + .uri("/foo") + .body(BodyInserters.fromMultipartData(multipartData)) + .exchange() + .expectBody() + .returnResult(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(request.getParts()).hasSize(1); + OperationRequestPart part = request.getParts().iterator().next(); + assertThat(part.getName()).isEqualTo("file"); + assertThat(part.getSubmittedFileName()).isEqualTo("image.png"); + assertThat(part.getHeaders().size()).isEqualTo(3); + assertThat(part.getHeaders().getContentLength()).isEqualTo(4); + ContentDisposition contentDisposition = part.getHeaders().getContentDisposition(); + assertThat(contentDisposition.getName()).isEqualTo("file"); + assertThat(contentDisposition.getFilename()).isEqualTo("image.png"); + assertThat(part.getHeaders().getContentType()).isEqualTo(MediaType.IMAGE_PNG); + assertThat(part.getContent()).containsExactly(1, 2, 3, 4); + } + + @Test + void requestWithCookies() { + ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) + .configureClient() + .baseUrl("http://localhost") + .build() + .get() + .uri("/foo") + .cookie("cookieName1", "cookieVal1") + .cookie("cookieName2", "cookieVal2") + .exchange() + .expectBody() + .returnResult(); + assertThat(result.getRequestHeaders().get(HttpHeaders.COOKIE)).isNotNull(); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.GET); + assertThat(request.getCookies()).hasSize(2); + assertThat(request.getHeaders().get(HttpHeaders.COOKIE)).isNull(); + assertThat(request.getCookies()).extracting("name").containsExactly("cookieName1", "cookieName2"); + assertThat(request.getCookies()).extracting("value").containsExactly("cookieVal1", "cookieVal2"); + } + +} diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java new file mode 100644 index 000000000..e50fc5a5d --- /dev/null +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.webtestclient; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.ResponseCookie; +import org.springframework.test.web.reactive.server.ExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; + +/** + * Tests for {@link WebTestClientResponseConverter}. + * + * @author Andy Wilkinson + */ +class WebTestClientResponseConverterTests { + + private final WebTestClientResponseConverter converter = new WebTestClientResponseConverter(); + + @Test + void basicResponse() { + ExchangeResult result = WebTestClient + .bindToRouterFunction( + RouterFunctions.route(GET("/foo"), (req) -> ServerResponse.ok().bodyValue("Hello, World!"))) + .configureClient() + .baseUrl("http://localhost") + .build() + .get() + .uri("/foo") + .exchange() + .expectBody() + .returnResult(); + OperationResponse response = this.converter.convert(result); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); + assertThat(response.getContentAsString()).isEqualTo("Hello, World!"); + assertThat(response.getHeaders().getContentType()) + .isEqualTo(MediaType.parseMediaType("text/plain;charset=UTF-8")); + assertThat(response.getHeaders().getContentLength()).isEqualTo(13); + } + + @Test + void responseWithCookie() { + ExchangeResult result = WebTestClient + .bindToRouterFunction(RouterFunctions.route(GET("/foo"), + (req) -> ServerResponse.ok() + .cookie(org.springframework.http.ResponseCookie.from("name", "value") + .domain("localhost") + .httpOnly(true) + .build()) + .build())) + .configureClient() + .baseUrl("http://localhost") + .build() + .get() + .uri("/foo") + .exchange() + .expectBody() + .returnResult(); + OperationResponse response = this.converter.convert(result); + assertThat(response.getHeaders().headerSet()).containsOnly( + entry(HttpHeaders.SET_COOKIE, Collections.singletonList("name=value; Domain=localhost; HttpOnly"))); + assertThat(response.getCookies()).hasSize(1); + assertThat(response.getCookies()).first().extracting(ResponseCookie::getName).isEqualTo("name"); + assertThat(response.getCookies()).first().extracting(ResponseCookie::getValue).isEqualTo("value"); + } + + @Test + void responseWithNonStandardStatusCode() { + ExchangeResult result = WebTestClient + .bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> ServerResponse.status(210).build())) + .configureClient() + .baseUrl("http://localhost") + .build() + .get() + .uri("/foo") + .exchange() + .expectBody() + .returnResult(); + OperationResponse response = this.converter.convert(result); + assertThat(response.getStatus()).isEqualTo(HttpStatusCode.valueOf(210)); + } + +} diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurerTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurerTests.java new file mode 100644 index 000000000..efa22ebfb --- /dev/null +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationConfigurerTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.webtestclient; + +import java.net.URI; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; + +import org.springframework.http.HttpMethod; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ExchangeFunction; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link WebTestClientRestDocumentationConfigurer}. + * + * @author Andy Wilkinson + */ +@ExtendWith(RestDocumentationExtension.class) +class WebTestClientRestDocumentationConfigurerTests { + + private WebTestClientRestDocumentationConfigurer configurer; + + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + this.configurer = new WebTestClientRestDocumentationConfigurer(restDocumentation); + + } + + @Test + void configurationCanBeRetrievedButOnlyOnce() { + ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test")) + .header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1") + .build(); + this.configurer.filter(request, mock(ExchangeFunction.class)); + assertThat(WebTestClientRestDocumentationConfigurer.retrieveConfiguration(request.headers())).isNotNull(); + assertThatIllegalStateException() + .isThrownBy(() -> WebTestClientRestDocumentationConfigurer.retrieveConfiguration(request.headers())); + } + + @Test + void requestUriHasDefaultsAppliedWhenItHasNoHost() { + ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/test?foo=bar#baz")) + .header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1") + .build(); + ExchangeFunction exchangeFunction = mock(ExchangeFunction.class); + this.configurer.filter(request, exchangeFunction); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ClientRequest.class); + verify(exchangeFunction).exchange(requestCaptor.capture()); + assertThat(requestCaptor.getValue().url()).isEqualTo(URI.create("http://localhost:8080/test?foo=bar#baz")); + } + + @Test + void requestUriIsNotChangedWhenItHasAHost() { + ClientRequest request = ClientRequest + .create(HttpMethod.GET, URI.create("https://api.example.com:4567/test?foo=bar#baz")) + .header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1") + .build(); + ExchangeFunction exchangeFunction = mock(ExchangeFunction.class); + this.configurer.filter(request, exchangeFunction); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ClientRequest.class); + verify(exchangeFunction).exchange(requestCaptor.capture()); + assertThat(requestCaptor.getValue().url()) + .isEqualTo(URI.create("https://api.example.com:4567/test?foo=bar#baz")); + } + +} diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationIntegrationTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationIntegrationTests.java new file mode 100644 index 000000000..30db5c4a6 --- /dev/null +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationIntegrationTests.java @@ -0,0 +1,317 @@ +/* + * Copyright 2014-2025 the original author or 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. + */ + +package org.springframework.restdocs.webtestclient; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseCookie; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.testfixtures.SnippetConditions; +import org.springframework.restdocs.testfixtures.SnippetConditions.CodeBlockCondition; +import org.springframework.restdocs.testfixtures.SnippetConditions.HttpResponseCondition; +import org.springframework.restdocs.testfixtures.SnippetConditions.TableCondition; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.FileSystemUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyExtractors; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration; +import static org.springframework.web.reactive.function.BodyInserters.fromValue; + +/** + * Integration tests for using Spring REST Docs with Spring Framework's WebTestClient. + * + * @author Andy Wilkinson + */ +@ExtendWith(RestDocumentationExtension.class) +public class WebTestClientRestDocumentationIntegrationTests { + + private WebTestClient webTestClient; + + @BeforeEach + void setUp(RestDocumentationContextProvider restDocumentation) { + RouterFunction route = RouterFunctions + .route(RequestPredicates.GET("/"), + (request) -> ServerResponse.status(HttpStatus.OK).body(fromValue(new Person("Jane", "Doe")))) + .andRoute(RequestPredicates.GET("/{foo}/{bar}"), + (request) -> ServerResponse.status(HttpStatus.OK).body(fromValue(new Person("Jane", "Doe")))) + .andRoute(RequestPredicates.POST("/upload"), + (request) -> request.body(BodyExtractors.toMultipartData()) + .map((parts) -> ServerResponse.status(HttpStatus.OK).build().block())) + .andRoute(RequestPredicates.GET("/set-cookie"), + (request) -> ServerResponse.ok() + .cookie(ResponseCookie.from("name", "value").domain("localhost").httpOnly(true).build()) + .build()); + this.webTestClient = WebTestClient.bindToRouterFunction(route) + .configureClient() + .baseUrl("https://api.example.com") + .filter(documentationConfiguration(restDocumentation)) + .build(); + } + + @Test + void defaultSnippetGeneration() { + File outputDir = new File("build/generated-snippets/default-snippets"); + FileSystemUtils.deleteRecursively(outputDir); + this.webTestClient.get() + .uri("/") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith(document("default-snippets")); + assertExpectedSnippetFilesExist(outputDir, "http-request.adoc", "http-response.adoc", "curl-request.adoc", + "httpie-request.adoc", "request-body.adoc", "response-body.adoc"); + } + + @Test + void pathParametersSnippet() { + this.webTestClient.get() + .uri("/{foo}/{bar}", "1", "2") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith( + document("path-parameters", pathParameters(parameterWithName("foo").description("Foo description"), + parameterWithName("bar").description("Bar description")))); + assertThat(new File("build/generated-snippets/path-parameters/path-parameters.adoc")).has(content( + tableWithTitleAndHeader(TemplateFormats.asciidoctor(), "+/{foo}/{bar}+", "Parameter", "Description") + .row("`foo`", "Foo description") + .row("`bar`", "Bar description"))); + } + + @Test + void queryParametersSnippet() { + this.webTestClient.get() + .uri("/?a=alpha&b=bravo") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith(document("query-parameters", + queryParameters(parameterWithName("a").description("Alpha description"), + parameterWithName("b").description("Bravo description")))); + assertThat(new File("build/generated-snippets/query-parameters/query-parameters.adoc")) + .has(content(tableWithHeader(TemplateFormats.asciidoctor(), "Parameter", "Description") + .row("`a`", "Alpha description") + .row("`b`", "Bravo description"))); + } + + @Test + void multipart() { + MultiValueMap multipartData = new LinkedMultiValueMap<>(); + multipartData.add("a", "alpha"); + multipartData.add("b", "bravo"); + Consumer> documentation = document("multipart", + requestParts(partWithName("a").description("Part a"), partWithName("b").description("Part b"))); + this.webTestClient.post() + .uri("/upload") + .body(BodyInserters.fromMultipartData(multipartData)) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith(documentation); + assertThat(new File("build/generated-snippets/multipart/request-parts.adoc")) + .has(content(tableWithHeader(TemplateFormats.asciidoctor(), "Part", "Description").row("`a`", "Part a") + .row("`b`", "Part b"))); + } + + @Test + void responseWithSetCookie() { + this.webTestClient.get() + .uri("/set-cookie") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith(document("set-cookie")); + assertThat(new File("build/generated-snippets/set-cookie/http-response.adoc")) + .has(content(httpResponse(TemplateFormats.asciidoctor(), HttpStatus.OK).header(HttpHeaders.SET_COOKIE, + "name=value; Domain=localhost; HttpOnly"))); + } + + @Test + void curlSnippetWithCookies() { + this.webTestClient.get() + .uri("/") + .cookie("cookieName", "cookieVal") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith(document("curl-snippet-with-cookies")); + assertThat(new File("build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ curl 'https://api.example.com/' -i -X GET \\%n" + + " -H 'Accept: application/json' \\%n" + " --cookie 'cookieName=cookieVal'")))); + } + + @Test + void curlSnippetWithEmptyParameterQueryString() { + this.webTestClient.get() + .uri("/?a=") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith(document("curl-snippet-with-empty-parameter-query-string")); + assertThat( + new File("build/generated-snippets/curl-snippet-with-empty-parameter-query-string/curl-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash").withContent(String + .format("$ curl 'https://api.example.com/?a=' -i -X GET \\%n" + " -H 'Accept: application/json'")))); + } + + @Test + void httpieSnippetWithCookies() { + this.webTestClient.get() + .uri("/") + .cookie("cookieName", "cookieVal") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith(document("httpie-snippet-with-cookies")); + assertThat(new File("build/generated-snippets/httpie-snippet-with-cookies/httpie-request.adoc")) + .has(content(codeBlock(TemplateFormats.asciidoctor(), "bash") + .withContent(String.format("$ http GET 'https://api.example.com/' \\%n" + + " 'Accept:application/json' \\%n" + " 'Cookie:cookieName=cookieVal'")))); + } + + @Test + void illegalStateExceptionShouldBeThrownWhenCallDocumentWebClientNotConfigured() { + assertThatThrownBy(() -> this.webTestClient + .mutateWith((builder, httpHandlerBuilder, connector) -> builder.filters(List::clear).build()) + .get() + .uri("/") + .exchange() + .expectBody() + .consumeWith(document("default-snippets"))).isInstanceOf(IllegalStateException.class) + .hasMessage("REST Docs configuration not found. Did you forget to register a " + + "WebTestClientRestDocumentationConfigurer as a filter?"); + } + + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { + Set actual = new HashSet<>(Arrays.asList(directory.listFiles())); + Set expected = Stream.of(snippets) + .map((snippet) -> new File(directory, snippet)) + .collect(Collectors.toSet()); + assertThat(actual).isEqualTo(expected); + } + + private Condition content(final Condition delegate) { + return new Condition<>() { + + @Override + public boolean matches(File value) { + try { + return delegate.matches(FileCopyUtils + .copyToString(new InputStreamReader(new FileInputStream(value), StandardCharsets.UTF_8))); + } + catch (IOException ex) { + fail("Failed to read '" + value + "'", ex); + return false; + } + } + + }; + } + + private CodeBlockCondition codeBlock(TemplateFormat format, String language) { + return SnippetConditions.codeBlock(format, language); + } + + private HttpResponseCondition httpResponse(TemplateFormat format, HttpStatus status) { + return SnippetConditions.httpResponse(format, status); + } + + private TableCondition tableWithHeader(TemplateFormat format, String... headers) { + return SnippetConditions.tableWithHeader(format, headers); + } + + private TableCondition tableWithTitleAndHeader(TemplateFormat format, String title, String... headers) { + return SnippetConditions.tableWithTitleAndHeader(format, title, headers); + } + + /** + * A person. + */ + public static class Person { + + private final String firstName; + + private final String lastName; + + Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return this.firstName; + } + + public String getLastName() { + return this.lastName; + } + + } + +}