diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..5ffbb2171b --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,54 @@ +name: "Code scanning - action" + +on: + push: + branches: [master, v1] + pull_request: + # The branches below must be a subset of the branches above + branches: [master, v1] + schedule: + - cron: '0 19 * * 1' + +jobs: + CodeQL-Build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + # Override language selection by uncommenting this and choosing your languages + with: + languages: java + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/maven-v1-pulls.yml b/.github/workflows/maven-v1-pulls.yml new file mode 100644 index 0000000000..81f4dcdb15 --- /dev/null +++ b/.github/workflows/maven-v1-pulls.yml @@ -0,0 +1,34 @@ +name: Build Test PR v1 + +on: + pull_request: + branches: [ "v1" ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + java: [ "8" ] + + steps: + - uses: actions/checkout@v4 + - name: Set up Java + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java }} + distribution: 'temurin' + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + - name: Cache local Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Build with Maven + run: | + ulimit -n 16384 + mvn -B verify --file pom.xml -Dsurefire.forkCount=4 -DargLine="-XX:-OmitStackTraceInFastThrow" -Dsurefire.useFile=false diff --git a/.github/workflows/maven-v1.yml b/.github/workflows/maven-v1.yml new file mode 100644 index 0000000000..d79d0d52cf --- /dev/null +++ b/.github/workflows/maven-v1.yml @@ -0,0 +1,53 @@ +name: Build Test Deploy v1 + +on: + push: + branches: [ "v1" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + java: [ 8 ] + + steps: + - uses: actions/checkout@v4 + - name: Set up Java + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java }} + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + distribution: 'temurin' + - name: Cache local Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Build with Maven, Deploy snapshot to maven central + run: | + ulimit -n 16384 + mvn --no-transfer-progress -B verify --file pom.xml -Dsurefire.forkCount=4 -DargLine="-XX:-OmitStackTraceInFastThrow" -Dsurefire.useFile=false + export MY_JAVA_VERSION=`java -version 2>&1 | head -1 | cut -d'"' -f2 | sed '/^1\./s///' | cut -d'.' -f1` + echo "JAVA VERSION" ${MY_JAVA_VERSION} + if [[ ${MY_JAVA_VERSION} == "8" ]]; + then + export MY_POM_VERSION=`mvn -q -Dexec.executable="echo" -Dexec.args='${projects.version}' --non-recursive org.codehaus.mojo:exec-maven-plugin:1.3.1:exec` + echo "POM VERSION" ${MY_POM_VERSION} + if [[ $MY_POM_VERSION =~ ^.*SNAPSHOT$ ]]; + then + mvn --no-transfer-progress -B clean deploy -Dsurefire.forkCount=4 -DargLine="-XX:-OmitStackTraceInFastThrow" -Dsurefire.useFile=false + else + echo "not deploying release: " ${MY_POM_VERSION} + fi + else + echo "not deploying on java version: " ${MY_JAVA_VERSION} + fi + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} diff --git a/.github/workflows/next-snapshot-v1.yml b/.github/workflows/next-snapshot-v1.yml new file mode 100644 index 0000000000..f3f5bdc415 --- /dev/null +++ b/.github/workflows/next-snapshot-v1.yml @@ -0,0 +1,88 @@ +name: Next Snapshot V1 + +on: + workflow_dispatch: + branches: ["v1"] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: tibdex/github-app-token@v1 + id: generate-token + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + - name: Set up Python 2.7 + uses: actions/setup-python@v2 + with: + python-version: 2.7 + - name: Set up Java 11 + uses: actions/setup-java@v4 + with: + java-version: 11 + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + distribution: 'temurin' + - name: Cache local Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Run pre release script + id: preRelease + run: | + ulimit -n 16384 + # export GPG_TTY=$(tty) + export MY_POM_VERSION=`mvn -q -Dexec.executable="echo" -Dexec.args='${projects.version}' --non-recursive org.codehaus.mojo:exec-maven-plugin:1.3.1:exec` + if [[ $MY_POM_VERSION =~ ^.*SNAPSHOT$ ]]; + then + echo "not releasing snapshot version: " ${MY_POM_VERSION} + echo "RELEASE_OK=no" >> $GITHUB_ENV + else + . ./CI/pre-release-v1.sh + echo "RELEASE_OK=yes" >> $GITHUB_ENV + fi + echo "SC_VERSION=$SC_VERSION" >> $GITHUB_ENV + echo "SC_NEXT_VERSION=$SC_NEXT_VERSION" >> $GITHUB_ENV + echo "SC_LAST_RELEASE=$SC_LAST_RELEASE" >> $GITHUB_ENV + - name: configure git user email + run: | + git config --global user.email "action@github.com" + git config --global user.name "GitHub Action" + git config --global hub.protocol https + git remote set-url origin https://\${{ secrets.GITHUB_TOKEN }}:x-oauth-basic@github.com/swagger-api/swagger-parser.git + - name: Checkout v1 + uses: actions/checkout@v2 + with: + ref: "v1" + fetch-depth: 0 + - name: Run next snapshot script + id: postRelease + if: env.RELEASE_OK == 'yes' + run: | + . ./CI/post-nextsnap-v1.sh + - name: Create Next Snapshot Pull Request + uses: peter-evans/create-pull-request@v4 + if: env.RELEASE_OK == 'yes' + with: + token: ${{ steps.generate-token.outputs.token }} + commit-message: bump snapshot ${{ env.SC_NEXT_VERSION }}-SNAPSHOT + title: 'bump snapshot ${{ env.SC_NEXT_VERSION }}-SNAPSHOT' + branch: bump-snap-${{ env.SC_NEXT_VERSION }}-SNAPSHOT + + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SC_VERSION: + SC_NEXT_VERSION: + GPG_PRIVATE_KEY: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + GPG_PASSPHRASE: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} diff --git a/.github/workflows/prepare-release-v1.yml b/.github/workflows/prepare-release-v1.yml new file mode 100644 index 0000000000..94349dcdf2 --- /dev/null +++ b/.github/workflows/prepare-release-v1.yml @@ -0,0 +1,68 @@ +name: Prepare Release V1 + +on: + workflow_dispatch: + branches: ["v1"] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: tibdex/github-app-token@v1 + id: generate-token + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Set up Java 8 + uses: actions/setup-java@v4 + with: + java-version: 8 + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + distribution: 'temurin' + - name: Cache local Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Run prepare release script + id: prepare-release + run: | + ulimit -n 16384 + export MY_POM_VERSION=`mvn -q -Dexec.executable="echo" -Dexec.args='${projects.version}' --non-recursive org.codehaus.mojo:exec-maven-plugin:1.3.1:exec` + if [[ $MY_POM_VERSION =~ ^.*SNAPSHOT$ ]]; + then + . ./CI/prepare-release-v1.sh + echo "PREPARE_RELEASE_OK=yes" >> $GITHUB_ENV + else + echo "not preparing release for release version: " ${MY_POM_VERSION} + echo "PREPARE_RELEASE_OK=no" >> $GITHUB_ENV + fi + echo "SC_VERSION=$SC_VERSION" >> $GITHUB_ENV + echo "SC_NEXT_VERSION=$SC_NEXT_VERSION" >> $GITHUB_ENV + - name: Create Prepare Release Pull Request + uses: peter-evans/create-pull-request@v4 + if: env.PREPARE_RELEASE_OK == 'yes' + with: + token: ${{ steps.generate-token.outputs.token }} + commit-message: prepare release ${{ env.SC_VERSION }} + title: 'prepare release ${{ env.SC_VERSION }}' + branch: prepare-release-${{ env.SC_VERSION }} + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SC_VERSION: + SC_NEXT_VERSION: + diff --git a/.github/workflows/release-v1.yml b/.github/workflows/release-v1.yml new file mode 100644 index 0000000000..0d957efc5d --- /dev/null +++ b/.github/workflows/release-v1.yml @@ -0,0 +1,88 @@ +name: Release V1 + +on: + workflow_dispatch: + branches: ["v1"] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: tibdex/github-app-token@v1 + id: generate-token + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Set up Java 8 + uses: actions/setup-java@v4 + with: + java-version: 8 + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + distribution: 'temurin' + gpg-private-key: ${{ secrets.OSSRH_GPG_PRIVATE_KEY }} + - name: Cache local Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Run pre release script + id: preRelease + run: | + ulimit -n 16384 + # export GPG_TTY=$(tty) + export MY_POM_VERSION=`mvn -q -Dexec.executable="echo" -Dexec.args='${projects.version}' --non-recursive org.codehaus.mojo:exec-maven-plugin:1.3.1:exec` + if [[ $MY_POM_VERSION =~ ^.*SNAPSHOT$ ]]; + then + echo "not releasing snapshot version: " ${MY_POM_VERSION} + echo "RELEASE_OK=no" >> $GITHUB_ENV + else + . ./CI/pre-release-v1.sh + echo "RELEASE_OK=yes" >> $GITHUB_ENV + fi + echo "SC_VERSION=$SC_VERSION" >> $GITHUB_ENV + echo "SC_NEXT_VERSION=$SC_NEXT_VERSION" >> $GITHUB_ENV + echo "SC_LAST_RELEASE=$SC_LAST_RELEASE" >> $GITHUB_ENV + - name: configure git user email + run: | + git config --global user.email "action@github.com" + git config --global user.name "GitHub Action" + git config --global hub.protocol https + git remote set-url origin https://\${{ secrets.GITHUB_TOKEN }}:x-oauth-basic@github.com/swagger-api/swagger-parser.git + - name: Run maven deploy/release + if: env.RELEASE_OK == 'yes' + run: | + mvn -DskipTests --no-transfer-progress -B -Prelease deploy + - name: Run post release script + id: postRelease + if: env.RELEASE_OK == 'yes' + run: | + . ./CI/post-release-v1.sh + - name: Create Next Snapshot Pull Request + uses: peter-evans/create-pull-request@v4 + if: env.RELEASE_OK == 'yes' + with: + token: ${{ steps.generate-token.outputs.token }} + commit-message: bump snapshot ${{ env.SC_NEXT_VERSION }}-SNAPSHOT + title: 'bump snapshot ${{ env.SC_NEXT_VERSION }}-SNAPSHOT' + branch: bump-snap-${{ env.SC_NEXT_VERSION }}-SNAPSHOT + + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SC_VERSION: + SC_NEXT_VERSION: + GPG_PRIVATE_KEY: ${{ secrets.OSSRH_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_PRIVATE_PASSPHRASE }} diff --git a/.gitignore b/.gitignore index 98aee58d6c..0dc9316ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,10 @@ atlassian-ide-plugin.xml .idea/ target/ - +.DS_Store .classpath .project .settings/ -**/test-output/* +**/test-output/ +dependency-reduced-pom.xml +*.pyc diff --git a/CI/CI.md b/CI/CI.md new file mode 100644 index 0000000000..f97a447f2f --- /dev/null +++ b/CI/CI.md @@ -0,0 +1,87 @@ +## Continuous integration + +### Build, test and deploy +Swagger Parser uses Github actions to run jobs/checks building, testing and deploying snapshots on push and PR events. + +These github actions are configured in `.github/workflows`: + +* maven.yml : Build Test Deploy master +* maven-pulls.yml Build Test PR +* maven-v1.yml : Build Test Deploy v1 (must exist in in `v1` branch) +* maven-v1-pulls.yml Build Test PR v1 (must exist in in `v1` branch) + + +These actions use available actions in combination with short bash scripts. + +### Release + +Releases are semi-automated and consist in 2 actions using available public actions in combination with bash and python scripts. +**TODO**: Python code is used for historical reasons to execute GitHub APIs calls, in general a more consistent environment would +be more maintainable e.g. implementing a custom JavaScript or Docker Container GitHub Action and/or a bash only script(s). + +#### Workflow summary + +1. execute `prepare-release-v1.yml` / `Prepare Release V1` for `V1` branch +1. check and merge the Prepare Release PR pushed by previous step. Delete the branch +1. execute `release-v1.yml` / `Release V1` for `V1` branch +1. check and merge the next snaphot PR pushed by previous step. Delete the branch + +#### Prepare Release + +The first action to execute is `prepare-release.yml` / `Prepare Release` for master, and +`prepare-release-v1.yml` / `Prepare Release V1` for `v1` branch. + +This is triggered by manually executing the action, selecting `Actions` in project GitHub UI, then `Prepare Release` workflow +and clicking `Run Workflow` (or `Prepare Release V1` and selecting `v1` in the dropdown) + +`Prepare Release` takes care of: + +* create release notes out of merged PRs +* Draft a release with related tag +* bump versions to release, and update all affected files +* build and test maven +* push a Pull Request with the changes for human check. + +After the PR checks complete, the PR can me merged, and the second phase `Release` started. + +#### Release + +Once prepare release PR has been merged, the second phase is provided by `release.yml` / `Release` actions for master, and +`release-v1.yml` / `Release V1` for `v1` branch. + +This is triggered by manually executing the action, selecting `Actions` in project GitHub UI, then `Release` workflow +and clicking `Run Workflow` (or `Release V1` and selecting `v1` in the dropdown) + +`Release` takes care of: + +* build and test maven +* deploy/publish to maven central +* publish the previously prepared GitHub release / tag +* push PR for next snapshot + + +### Secrets + +GitHub Actions make use of `Secrets` which can be configured either with Repo or Organization scope; the needed secrets are the following: + +* `APP_ID` and APP_PRIVATE_KEY`: these are the values provided by an account configured GitHub App, allowing to obtain a GitHub token +different from the default used in GitHub Actions (which does not allow to "chain" actions).Actions + +The GitHub App must be configured as detailed in [this doc](https://github.com/peter-evans/create-pull-request/blob/master/docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens). + +See also [here](https://github.com/peter-evans/create-pull-request/blob/master/docs/concepts-guidelines.md#triggering-further-workflow-runs) + +* `OSSRH_GPG_PRIVATE_KEY` and `OSSRH_GPG_PRIVATE_PASSPHRASE` : gpg key and passphrase to be used for sonatype releases +GPG private key and passphrase defined to be used for sonatype deployments, as detailed in +https://central.sonatype.org/pages/working-with-pgp-signatures.html (I'd say with email matching the one of the sonatype account of point 1 + +* `OSSRH_USERNAME` and `OSSRH_TOKEN`: sonatype user/token + + + + + + + + + diff --git a/CI/ghApiClient.py b/CI/ghApiClient.py new file mode 100755 index 0000000000..fcec1eace8 --- /dev/null +++ b/CI/ghApiClient.py @@ -0,0 +1,59 @@ +#!/usr/bin/python + +import os +import time +import urllib.request, urllib.error, urllib.parse +import http.client +import json + +GH_BASE_URL = "https://api.github.com/" + +GH_TOKEN = os.environ['GH_TOKEN'] +GH_AUTH = "Bearer %s" % GH_TOKEN + +def readUrl(name): + try: + request = urllib.request.Request(GH_BASE_URL + name) + request.add_header("Authorization", GH_AUTH) + content = urllib.request.urlopen(request).read() + jcont = json.loads(content) + return jcont + except urllib.error.HTTPError as e: + print(('HTTPError = ' + str(e.code))) + raise e + except urllib.error.URLError as e: + print(('URLError = ' + str(e.reason))) + raise e + except http.client.HTTPException as e: + print(('HTTPException = ' + str(e))) + raise e + except Exception: + import traceback + print(('generic exception: ' + traceback.format_exc())) + raise IOError + +def postUrl(name, body): + global GH_BASE_URL + try: + time.sleep(0.05) + request = urllib.request.Request(GH_BASE_URL + name) + request.add_header("Authorization", GH_AUTH) + request.add_header("Accept", "application/vnd.github.v3+json") + data = body.encode('utf-8') + content = urllib.request.urlopen(request, data).read() + jcont = json.loads(content) + return jcont + except urllib.error.HTTPError as e: + print(('HTTPError = ' + str(e.code))) + print((str(e))) + raise e + except urllib.error.URLError as e: + print(('URLError = ' + str(e.reason))) + raise e + except http.client.HTTPException as e: + print(('HTTPException = ' + str(e))) + raise e + except Exception: + import traceback + print(('generic exception: ' + traceback.format_exc())) + raise IOError diff --git a/CI/lastReleaseV1.py b/CI/lastReleaseV1.py new file mode 100755 index 0000000000..bb02b84e8d --- /dev/null +++ b/CI/lastReleaseV1.py @@ -0,0 +1,20 @@ +#!/usr/bin/python + +import ghApiClient + +def getLastReleaseTag(): + content = ghApiClient.readUrl('repos/swagger-api/swagger-parser/releases') + for l in content: + draft = l["draft"] + tag = l["tag_name"] + if str(draft) != 'True' and tag.startswith("v1"): + return tag[1:] + +# main +def main(): + result = getLastReleaseTag() + print(result) + +# here start main +main() + diff --git a/CI/post-nextsnap-v1.sh b/CI/post-nextsnap-v1.sh new file mode 100755 index 0000000000..747fde9276 --- /dev/null +++ b/CI/post-nextsnap-v1.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +CUR=$(pwd) +TMPDIR="$(dirname -- "${0}")" + +SC_RELEASE_TAG="v$SC_VERSION" + +##################### +### update the version to next snapshot in maven project with set version +##################### +mvn versions:set -DnewVersion="${SC_NEXT_VERSION}-SNAPSHOT" +mvn versions:commit diff --git a/CI/post-release-v1.sh b/CI/post-release-v1.sh new file mode 100755 index 0000000000..d849ec253b --- /dev/null +++ b/CI/post-release-v1.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +CUR=$(pwd) +TMPDIR="$(dirname -- "${0}")" + +SC_RELEASE_TAG="v$SC_VERSION" + +##################### +### publish pre-prepared release (tag is created) +##################### +python $CUR/CI/publishReleaseV1.py "$SC_RELEASE_TAG" + +##################### +### update the version to next snapshot in maven project with set version +##################### +mvn versions:set -DnewVersion="${SC_NEXT_VERSION}-SNAPSHOT" +mvn versions:commit diff --git a/CI/pre-release-v1.sh b/CI/pre-release-v1.sh new file mode 100755 index 0000000000..bb22fc6d37 --- /dev/null +++ b/CI/pre-release-v1.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +CUR=$(pwd) + +export SC_VERSION=`mvn -q -Dexec.executable="echo" -Dexec.args='${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}' --non-recursive build-helper:parse-version org.codehaus.mojo:exec-maven-plugin:1.3.1:exec` +export SC_NEXT_VERSION=`mvn -q -Dexec.executable="echo" -Dexec.args='${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.nextIncrementalVersion}' --non-recursive build-helper:parse-version org.codehaus.mojo:exec-maven-plugin:1.3.1:exec` +SC_QUALIFIER=`mvn -q -Dexec.executable="echo" -Dexec.args='${parsedVersion.qualifier}' --non-recursive build-helper:parse-version org.codehaus.mojo:exec-maven-plugin:1.3.1:exec` +#SC_LAST_RELEASE=`mvn -q -Dexec.executable="echo" -Dexec.args='${releasedVersion.version}' --non-recursive org.codehaus.mojo:build-helper-maven-plugin:3.2.0:released-version org.codehaus.mojo:exec-maven-plugin:1.3.1:exec` +SC_LAST_RELEASE=`python $CUR/CI/lastReleaseV1.py` + + +SC_RELEASE_TAG="v$SC_VERSION" + + +##################### +### build and test maven ### +##################### +ulimit -n 16384 +mvn --no-transfer-progress -B install --file pom.xml -Dsurefire.forkCount=4 -DargLine="-XX:-OmitStackTraceInFastThrow" -Dsurefire.useFile=false diff --git a/CI/prepare-release-v1.sh b/CI/prepare-release-v1.sh new file mode 100755 index 0000000000..5bd86e4f44 --- /dev/null +++ b/CI/prepare-release-v1.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +CUR=$(pwd) + +export SC_VERSION=`mvn -q -Dexec.executable="echo" -Dexec.args='${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}' --non-recursive build-helper:parse-version org.codehaus.mojo:exec-maven-plugin:1.3.1:exec` +export SC_NEXT_VERSION=`mvn -q -Dexec.executable="echo" -Dexec.args='${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.nextIncrementalVersion}' --non-recursive build-helper:parse-version org.codehaus.mojo:exec-maven-plugin:1.3.1:exec` +SC_QUALIFIER=`mvn -q -Dexec.executable="echo" -Dexec.args='${parsedVersion.qualifier}' --non-recursive build-helper:parse-version org.codehaus.mojo:exec-maven-plugin:1.3.1:exec` +#SC_LAST_RELEASE=`mvn -q -Dexec.executable="echo" -Dexec.args='${releasedVersion.version}' --non-recursive org.codehaus.mojo:build-helper-maven-plugin:3.2.0:released-version org.codehaus.mojo:exec-maven-plugin:1.3.1:exec` +SC_LAST_RELEASE=`python $CUR/CI/lastReleaseV1.py` + + + +SC_RELEASE_TITLE="Swagger-parser $SC_VERSION released!" +SC_RELEASE_TAG="v$SC_VERSION" + + +##################### +### draft release Notes with next release after last release, with tag +##################### +python $CUR/CI/releaseNotesV1.py "$SC_LAST_RELEASE" "$SC_RELEASE_TITLE" "$SC_RELEASE_TAG" + +##################### +### update the version to release in maven project with set version +##################### +mvn versions:set -DnewVersion=$SC_VERSION +mvn versions:commit + +##################### +### update all other versions in files around to the new release, including readme ### +##################### +sc_find="$SC_LAST_RELEASE" +sc_replace="$SC_VERSION" +sed -i -e "s/$sc_find/$sc_replace/g" $CUR/README.md + + +##################### +### build and test maven ### +##################### +ulimit -n 16384 +mvn --no-transfer-progress -B install --file pom.xml -Dsurefire.forkCount=4 -DargLine="-XX:-OmitStackTraceInFastThrow" -Dsurefire.useFile=false + diff --git a/CI/publishReleaseV1.py b/CI/publishReleaseV1.py new file mode 100755 index 0000000000..9b026217db --- /dev/null +++ b/CI/publishReleaseV1.py @@ -0,0 +1,28 @@ +#!/usr/bin/python + +import sys +import ghApiClient + +def lastReleaseId(tag): + content = ghApiClient.readUrl('repos/swagger-api/swagger-parser/releases') + for l in content: + draft = l["draft"] + draft_tag = l["tag_name"] + if str(draft) == 'True' and tag == draft_tag: + return l["id"] + +def publishRelease(tag): + id = lastReleaseId(tag) + payload = "{\"tag_name\":\"" + tag + "\", " + payload += "\"draft\":" + "false" + ", " + payload += "\"target_commitish\":\"" + "v1" + "\"}" + content = ghApiClient.postUrl('repos/swagger-api/swagger-parser/releases/' + str(id), payload) + return content + +# main +def main(tag): + publishRelease (tag) + +# here start main +main(sys.argv[1]) + diff --git a/CI/releaseNotesV1.py b/CI/releaseNotesV1.py new file mode 100755 index 0000000000..eb501a8558 --- /dev/null +++ b/CI/releaseNotesV1.py @@ -0,0 +1,52 @@ +#!/usr/bin/python + +import sys +import json +from datetime import datetime +import ghApiClient + +def allPulls(releaseDate): + + result = "" + + baseurl = "https://api.github.com/repos/swagger-api/swagger-parser/pulls/" + content = ghApiClient.readUrl('repos/swagger-api/swagger-parser/pulls?state=closed&base=v1&per_page=100') + for l in content: + stripped = l["url"][len(baseurl):] + mergedAt = l["merged_at"] + if mergedAt is not None: + if datetime.strptime(mergedAt, '%Y-%m-%dT%H:%M:%SZ') > releaseDate: + if not l['title'].startswith("bump snap"): + result += '\n' + result += "* " + l['title'] + " (#" + stripped + ")" + return result + + +def lastReleaseDate(tag): + content = ghApiClient.readUrl('repos/swagger-api/swagger-parser/releases/tags/' + tag) + publishedAt = content["published_at"] + return datetime.strptime(publishedAt, '%Y-%m-%dT%H:%M:%SZ') + + +def addRelease(release_title, tag, content): + payload = "{\"tag_name\":\"" + tag + "\", " + payload += "\"name\":" + json.dumps(release_title) + ", " + payload += "\"body\":" + json.dumps(content) + ", " + payload += "\"draft\":" + "true" + ", " + payload += "\"prerelease\":" + "false" + ", " + payload += "\"target_commitish\":\"" + "v1" + "\"}" + content = ghApiClient.postUrl('repos/swagger-api/swagger-parser/releases', payload) + return content + +def getReleases(): + content = ghApiClient.readUrl('repos/swagger-api/swagger-parser/releases') + return content + +# main +def main(last_release, release_title, tag): + result = allPulls(lastReleaseDate('v' + last_release)) + addRelease (release_title, tag, result) + +# here start main +main(sys.argv[1], sys.argv[2], sys.argv[3]) + diff --git a/LICENSE b/LICENSE index 252389a1a9..01abb442b9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,11 +1,201 @@ -Copyright 2017 SmartBear Software + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at [apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + 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 2020 SmartBear Software 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 + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 8b3f117b4b..3ed586eafe 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -**NOTE:** If you're looking for swagger=parser 1.X and OpenApi 2.0, please refer to [v1 branch](https://github.com/swagger-api/swagger-core/tree/v1) +**NOTE:** If you're looking for `swagger-parser` 2.X and OpenApi 3.0, please refer to [master branch](https://github.com/swagger-api/swagger-parser) # Swagger Parser -[![Build Status](https://img.shields.io/jenkins/s/https/jenkins.swagger.io/view/OSS%20-%20Java/job/oss-swagger-parser-v1.svg)](https://jenkins.swagger.io/view/OSS%20-%20Java/job/oss-swagger-parser-v1) +![Build Test Deploy v1](https://github.com/swagger-api/swagger-parser/workflows/Build%20Test%20Deploy%20v1/badge.svg?branch=v1) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.swagger/swagger-parser/badge.svg?style=plastic)](https://maven-badges.herokuapp.com/maven-central/io.swagger/swagger-parser) [![PR Stats](https://img.shields.io/github/issues-pr/swagger-api/swagger-parser.svg)](https://github.com/swagger-api/swagger-parser/pulls) [![Issue Stats](https://img.shields.io/github/issues/swagger-api/swagger-parser.svg)](https://github.com/swagger-api/swagger-parser/issues) + ## Overview This is the swagger parser project, which reads OpenAPI Specifications into current Java POJOs. It also provides a simple framework to add additional converters from different formats into the Swagger objects, making the entire toolchain available. @@ -96,7 +97,7 @@ But... this is all standard SSL configuration stuff and is well documented acros ### Prerequisites You need the following installed and available in your $PATH: -* [Java 1.7](http://java.oracle.com) +* [Java 8](http://java.oracle.com) * [Apache maven 3.0.3 or greater](http://maven.apache.org/) After cloning the project, you can build it from source with this command: @@ -117,7 +118,7 @@ You can include this library from Sonatype OSS for SNAPSHOTS, or Maven central f io.swagger swagger-parser - 1.0.40-SNAPSHOT + 1.0.75 ``` @@ -127,26 +128,11 @@ To add swagger parsing support for older versions of swagger, add the `compat` m io.swagger swagger-compat-spec-parser - 1.0.40-SNAPSHOT + 1.0.75 ``` +## Security contact -License -------- - -Copyright 2017 SmartBear Software - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at [apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - ---- - +Please disclose any security-related issues or vulnerabilities by emailing [security@swagger.io](mailto:security@swagger.io), instead of using the public issue tracker. diff --git a/modules/swagger-compat-spec-parser/pom.xml b/modules/swagger-compat-spec-parser/pom.xml index 0045f67ffe..697e2a7e2a 100644 --- a/modules/swagger-compat-spec-parser/pom.xml +++ b/modules/swagger-compat-spec-parser/pom.xml @@ -4,7 +4,7 @@ io.swagger swagger-parser-project - 1.0.40-SNAPSHOT + 1.0.76-SNAPSHOT ../.. 4.0.0 @@ -12,6 +12,7 @@ swagger-compat-spec-parser jar swagger-compat-spec-parser + swagger-compat-spec-parser io.swagger @@ -26,12 +27,12 @@ com.github.java-json-tools json-schema-validator - 2.2.8 + 2.2.14 - com.github.fge + com.github.java-json-tools json-patch - 1.6 + 1.13 junit @@ -42,7 +43,7 @@ org.testng testng - 6.8.7 + ${testng-version} test @@ -62,7 +63,7 @@ org.apache.httpcomponents httpclient - 4.5.2 + 4.5.14 org.jmockit diff --git a/modules/swagger-compat-spec-parser/src/main/java/io/swagger/parser/SwaggerCompatConverter.java b/modules/swagger-compat-spec-parser/src/main/java/io/swagger/parser/SwaggerCompatConverter.java index ac28712d79..32ea97bdf8 100644 --- a/modules/swagger-compat-spec-parser/src/main/java/io/swagger/parser/SwaggerCompatConverter.java +++ b/modules/swagger-compat-spec-parser/src/main/java/io/swagger/parser/SwaggerCompatConverter.java @@ -70,7 +70,6 @@ import java.math.BigDecimal; import java.net.URI; import java.nio.file.Files; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -196,9 +195,9 @@ public ResourceListing readResourceListing(String input, MessageBuilder messages final String fileScheme = "file:"; java.nio.file.Path path; if (input.toLowerCase().startsWith(fileScheme)) { - path = Paths.get(URI.create(input)); + path = java.nio.file.Paths.get(URI.create(input)); } else { - path = Paths.get(input); + path = java.nio.file.Paths.get(input); } String json; if (Files.exists(path)) { @@ -210,7 +209,7 @@ public ResourceListing readResourceListing(String input, MessageBuilder messages jsonNode = Json.mapper().readTree(json); } - if (jsonNode.get("swaggerVersion") == null) { + if (jsonNode == null || jsonNode.get("swaggerVersion") == null) { return null; } ResourceListingMigrator migrator = new ResourceListingMigrator(); @@ -515,9 +514,9 @@ public ApiDeclaration readDeclaration(String input, MessageBuilder messages, Lis final String fileScheme = "file:"; java.nio.file.Path path; if (input.toLowerCase().startsWith(fileScheme)) { - path = Paths.get(URI.create(input)); + path = java.nio.file.Paths.get(URI.create(input)); } else { - path = Paths.get(input); + path = java.nio.file.Paths.get(input); } String json; if (Files.exists(path)) { @@ -571,7 +570,6 @@ public Swagger convert(ResourceListing resourceListing, List api info = new Info() .version(resourceListing.getApiVersion()); } - Map paths = new HashMap(); Map definitions = new HashMap(); String basePath = null; diff --git a/modules/swagger-parser-safe-url-resolver/README.md b/modules/swagger-parser-safe-url-resolver/README.md new file mode 100644 index 0000000000..916c9bbac4 --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/README.md @@ -0,0 +1,71 @@ +# Swagger Parser Safe URL Resolver + +The `swagger-parser-safe-url-resolver` is a library used for verifying that the hostname of URLs does not resolve to a private/restricted IPv4/IPv6 address range. +This library can be used in services that deal with user-submitted URLs that get fetched (like in swagger-parser when resolving external URL $refs) to protect against Server-Side Request Forgery and DNS rebinding attacks. + +## How does it work? +The main class of the package is the `PermittedUrlsChecker` which has one method: `verify(String url)`. +This method takes in a string URL and performs the following steps: + +1. Gets the hostname portion from the URL +2. Resolves the hostname to an IP address +3. Checks if that IP address is in a private/restricted IP address range (and throws an exception if it is) +4. Returns a `ResolvedUrl` object which contains + 4.1. `String url` where the original URL has the hostname replaced with the IP address + 4.2. A `String hostHeader` which contains the hostname from the original URL to be added as a host header + +This behavior can also be customized with the allowlist and denylist in the constructor, whereby: + +- An entry in the allowlist will allow the URL to pass even if it resolves to a private/restricted IP address +- An entry in the denylist will throw an exception even when the URL resolves to a public IP address + +## Installation +Add the following to you `pom.xml` file under `dependencies` +```xml + + io.swagger + swagger-parser-safe-url-resolver + // version of swagger-parser being used + ${swagger-parser-v1-version} + +``` + +## Example usage + +```java +import io.swagger.parser.urlresolver.PermittedUrlsChecker; +import io.swagger.parser.urlresolver.exceptions.HostDeniedException; +import io.swagger.parser.urlresolver.models.ResolvedUrl; + +import java.util.List; + +public class Main { + public static void main() { + List allowlist = List.of("mysite.local"); + List denylist = List.of("*.example.com:443"); + var checker = new PermittedUrlsChecker(allowlist, denylist); + + try { + // Will throw a HostDeniedException as `localhost` + // resolves to local IP and is not in allowlist + checker.verify("http://localhost/example"); + + // Will return a ResolvedUrl if `github.com` + // resolves to a public IP + checker.verify("https://github.com/swagger-api/swagger-parser"); + + // Will throw a HostDeniedException as `*.example.com` is + // explicitly deny listed, even if it resolves to public IP + checker.verify("https://subdomain.example.com/somepage"); + + // Will return a `ResolvedUrl` as `mysite.local` + // is explicitly allowlisted + ResolvedUrl resolvedUrl = checker.verify("http://mysite.local/example"); + System.out.println(resolvedUrl.getUrl()); // "http://127.0.0.1/example" + System.out.println(resolvedUrl.getHostHeader()); // "mysite.local" + } catch (HostDeniedException e) { + e.printStackTrace(); + } + } +} +``` \ No newline at end of file diff --git a/modules/swagger-parser-safe-url-resolver/pom.xml b/modules/swagger-parser-safe-url-resolver/pom.xml new file mode 100644 index 0000000000..795db3df01 --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/pom.xml @@ -0,0 +1,47 @@ + + + + io.swagger + swagger-parser-project + 1.0.76-SNAPSHOT + ../.. + + 4.0.0 + swagger-parser-safe-url-resolver + jar + swagger-parser-safe-url-resolver + swagger-parser-safe-url-resolver + + + commons-io + commons-io + ${commons-io-version} + + + org.slf4j + slf4j-simple + ${slf4j-version} + test + + + org.testng + testng + ${testng-version} + test + + + junit + junit + ${junit-version} + test + + + org.jmockit + jmockit + ${jmockit-version} + test + + + \ No newline at end of file diff --git a/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/PermittedUrlsChecker.java b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/PermittedUrlsChecker.java new file mode 100644 index 0000000000..34ee401f81 --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/PermittedUrlsChecker.java @@ -0,0 +1,106 @@ +package io.swagger.parser.urlresolver; + +import io.swagger.parser.urlresolver.exceptions.HostDeniedException; +import io.swagger.parser.urlresolver.matchers.UrlPatternMatcher; +import io.swagger.parser.urlresolver.models.ResolvedUrl; +import io.swagger.parser.urlresolver.utils.NetUtils; + +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.List; + +public class PermittedUrlsChecker { + + protected final UrlPatternMatcher allowlistMatcher; + protected final UrlPatternMatcher denylistMatcher; + + public PermittedUrlsChecker() { + this.allowlistMatcher = new UrlPatternMatcher(Collections.emptyList()); + this.denylistMatcher = new UrlPatternMatcher(Collections.emptyList()); + } + + public PermittedUrlsChecker(List allowlist, List denylist) { + if(allowlist != null) { + this.allowlistMatcher = new UrlPatternMatcher(allowlist); + } else { + this.allowlistMatcher = new UrlPatternMatcher(Collections.emptyList()); + } + + if(denylist != null) { + this.denylistMatcher = new UrlPatternMatcher(denylist); + } else { + this.denylistMatcher = new UrlPatternMatcher(Collections.emptyList()); + } + } + + public ResolvedUrl verify(String url) throws HostDeniedException { + URL parsed; + + try { + parsed = new URL(url); + } catch (MalformedURLException e) { + throw new HostDeniedException(String.format("Failed to parse URL. URL [%s]", url), e); + } + + if (!parsed.getProtocol().equals("http") && !parsed.getProtocol().equals("https")) { + throw new HostDeniedException(String.format("URL does not use a supported protocol. URL [%s]", url)); + } + + String hostname; + try { + hostname = NetUtils.getHostFromUrl(url); + } catch (MalformedURLException e) { + throw new HostDeniedException(String.format("Failed to get hostname from URL. URL [%s]", url), e); + } + + if (this.allowlistMatcher.matches(url)) { + return new ResolvedUrl(url, hostname); + } + + if (this.denylistMatcher.matches(url)) { + throw new HostDeniedException(String.format("URL is part of the explicit denylist. URL [%s]", url)); + } + + InetAddress ip; + try { + ip = NetUtils.getHostByName(hostname); + } catch (UnknownHostException e) { + throw new HostDeniedException( + String.format("Failed to resolve IP from hostname. Hostname [%s]", hostname), e); + } + + String urlWithIp; + try { + urlWithIp = NetUtils.setHost(url, ip.getHostAddress()); + } catch (MalformedURLException e) { + throw new HostDeniedException( + String.format("Failed to create new URL with IP. IP [%s] URL [%s]", ip.getHostAddress(), url), e); + } + + if (this.allowlistMatcher.matches(urlWithIp)) { + return new ResolvedUrl(urlWithIp, hostname); + } + + if (isRestrictedIpRange(ip)) { + throw new HostDeniedException(String.format("IP is restricted. URL [%s]", urlWithIp)); + } + + if (this.denylistMatcher.matches(urlWithIp)) { + throw new HostDeniedException(String.format("IP is part of the explicit denylist. URL [%s]", urlWithIp)); + } + + return new ResolvedUrl(urlWithIp, hostname); + } + + protected boolean isRestrictedIpRange(InetAddress ip) { + return ip.isLinkLocalAddress() + || ip.isSiteLocalAddress() + || ip.isLoopbackAddress() + || ip.isAnyLocalAddress() + || NetUtils.isUniqueLocalAddress(ip) + || NetUtils.isNAT64Address(ip); + } +} diff --git a/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/exceptions/HostDeniedException.java b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/exceptions/HostDeniedException.java new file mode 100644 index 0000000000..d7c34e054e --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/exceptions/HostDeniedException.java @@ -0,0 +1,11 @@ +package io.swagger.parser.urlresolver.exceptions; + +public class HostDeniedException extends Exception { + public HostDeniedException(String message) { + super(message); + } + + public HostDeniedException(String message, Throwable e) { + super(message, e); + } +} diff --git a/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/matchers/UrlPatternMatcher.java b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/matchers/UrlPatternMatcher.java new file mode 100644 index 0000000000..2348ecf7ce --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/matchers/UrlPatternMatcher.java @@ -0,0 +1,60 @@ +package io.swagger.parser.urlresolver.matchers; + +import io.swagger.parser.urlresolver.utils.NetUtils; + +import java.net.IDN; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.io.FilenameUtils.wildcardMatch; + +public class UrlPatternMatcher { + + private final List patterns; + + public UrlPatternMatcher(List patterns) { + this.patterns = new ArrayList<>(); + + patterns.forEach(pattern -> { + String patternLower = pattern.toLowerCase(); + String hostAndPort = pattern.contains(":") ? patternLower : patternLower + ":*"; + String[] split = hostAndPort.split(":"); + String host = Character.isDigit(split[0].charAt(0)) ? split[0] : IDN.toASCII(split[0], IDN.ALLOW_UNASSIGNED); + String port = split.length > 1 ? split[1] : "*"; + + // Ignore domains that end in a wildcard + if (host.length() > 1 && !NetUtils.isIPv4(host.replace("*", "0")) && host.endsWith("*")) { + return; + } + + this.patterns.add(String.format("%s:%s", host, port)); + }); + } + + public boolean matches(String url) { + URL parsed; + try { + parsed = new URL(url.toLowerCase()); + } catch (MalformedURLException e) { + return false; + } + + String host = IDN.toASCII(parsed.getHost(), IDN.ALLOW_UNASSIGNED); + String hostAndPort; + if (parsed.getPort() == -1) { + if (parsed.getProtocol().equals("http")) { + hostAndPort = host + ":80"; + } else if (parsed.getProtocol().equals("https")) { + hostAndPort = host + ":443"; + } else { + return false; + } + } else { + hostAndPort = host + ":" + parsed.getPort(); + } + + return this.patterns.stream().anyMatch(pattern -> wildcardMatch(hostAndPort, pattern)); + } +} diff --git a/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/models/ResolvedUrl.java b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/models/ResolvedUrl.java new file mode 100644 index 0000000000..b7fa7218f7 --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/models/ResolvedUrl.java @@ -0,0 +1,36 @@ +package io.swagger.parser.urlresolver.models; + +public class ResolvedUrl { + + private String url; + private String hostHeader; + + public ResolvedUrl(String url, String hostHeader) { + this.url = url; + this.hostHeader = hostHeader; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getHostHeader() { + return hostHeader; + } + + public void setHostHeader(String hostHeader) { + this.hostHeader = hostHeader; + } + + @Override + public String toString() { + return "ResolvedUrl{" + + "url='" + url + '\'' + + ", hostHeader='" + hostHeader + '\'' + + '}'; + } +} diff --git a/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/utils/NetUtils.java b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/utils/NetUtils.java new file mode 100644 index 0000000000..c8f179c986 --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/main/java/io/swagger/parser/urlresolver/utils/NetUtils.java @@ -0,0 +1,90 @@ +package io.swagger.parser.urlresolver.utils; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.UnknownHostException; + +public class NetUtils { + + private NetUtils() {} + + public static InetAddress getHostByName(String hostname) throws UnknownHostException { + return InetAddress.getByName(hostname); + } + + public static String getHostFromUrl(String url) throws MalformedURLException { + String hostnameOrIP = new URL(url).getHost(); + //IPv6 addresses in URLs are surrounded by square brackets + if (hostnameOrIP.length() > 2 && hostnameOrIP.startsWith("[") && hostnameOrIP.endsWith("]")) { + return hostnameOrIP.substring(1, hostnameOrIP.length() - 1); + } + return hostnameOrIP; + } + + public static String setHost(String url, String host) throws MalformedURLException { + URL parsed = new URL(url); + if (isIPv6(host)) { + return url.replace(parsed.getHost(), "[" + host + "]"); + } else { + return url.replace(parsed.getHost(), host); + } + } + + public static boolean isIPv4(String ipAddress) { + boolean isIPv4 = false; + + if (ipAddress != null) { + try { + InetAddress inetAddress = InetAddress.getByName(ipAddress); + isIPv4 = (inetAddress instanceof Inet4Address); + } catch (UnknownHostException ignored) { + return false; + } + } + + return isIPv4; + } + + public static boolean isIPv6(String ipAddress) { + boolean isIPv6 = false; + + if (ipAddress != null) { + try { + InetAddress inetAddress = InetAddress.getByName(ipAddress); + isIPv6 = (inetAddress instanceof Inet6Address); + } catch (UnknownHostException ignored) { + return false; + } + } + + return isIPv6; + } + + // Not picked up by Inet6Address.is*Address() checks + public static boolean isUniqueLocalAddress(InetAddress ip) { + // Only applies to IPv6 + if (ip instanceof Inet4Address) { + return false; + } + + byte[] address = ip.getAddress(); + return (address[0] & 0xff) == 0xfc || (address[0] & 0xff) == 0xfd; + } + + // Not picked up by Inet6Address.is*Address() checks + public static boolean isNAT64Address(InetAddress ip) { + // Only applies to IPv6 + if (ip instanceof Inet4Address) { + return false; + } + + byte[] address = ip.getAddress(); + return (address[0] & 0xff) == 0x00 + && (address[1] & 0xff) == 0x64 + && (address[2] & 0xff) == 0xff + && (address[3] & 0xff) == 0x9b; + } +} diff --git a/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/parser/urlresolver/PermittedUrlsCheckerTest.java b/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/parser/urlresolver/PermittedUrlsCheckerTest.java new file mode 100644 index 0000000000..e87b0ca1be --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/parser/urlresolver/PermittedUrlsCheckerTest.java @@ -0,0 +1,283 @@ +package io.swagger.parser.urlresolver; + +import io.swagger.parser.urlresolver.exceptions.HostDeniedException; +import io.swagger.parser.urlresolver.models.ResolvedUrl; +import io.swagger.parser.urlresolver.utils.NetUtils; +import mockit.*; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + + +import java.net.InetAddress; +import java.util.Collections; +import java.util.List; + +public class PermittedUrlsCheckerTest { + + private final List emptyAllowlist = Collections.emptyList(); + private final List emptyDenylist = Collections.emptyList(); + @Mocked + private NetUtils netUtils; + + private PermittedUrlsChecker checker; + + @BeforeMethod + void beforeMethod() { + this.checker = new PermittedUrlsChecker(Collections.emptyList(), Collections.emptyList()); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*IP is restricted.*") + public void shouldRejectPrivateSIITIPv4in6HostReferencesInABCDFormat() throws Exception { + String url = "https://[0:0:0:0:0:ffff:10.1.33.147]:8000/v1/operation?theThing=something"; + String expectedIp = "10.1.33.147"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedIp; + NetUtils.getHostByName(expectedIp); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = url; + }}; + + checker.verify(url); + } + + @Test + public void shouldAllowPublicSIITIPv4in6HostReferencesInABCDFormat() throws Exception { + String url = "https://[0:0:0:0:0:ffff:1.2.3.4]:8000/v1/operation?theThing=something"; + String expectedIp = "1.2.3.4"; + String expectedUrl = "https://1.2.3.4:8000/v1/operation?theThing=something"; + String expectedHostHeader = "0:0:0:0:0:ffff:1.2.3.4"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHostHeader; + NetUtils.getHostByName(expectedHostHeader); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = expectedUrl; + }}; + + ResolvedUrl result = checker.verify(url); + + Assert.assertEquals(result.getUrl(), expectedUrl); + Assert.assertEquals(result.getHostHeader(), expectedHostHeader); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*IP is restricted.*") + public void shouldRejectPrivateSIITIPv4in6HostReferencesInIPv6Format() throws Exception { + String url = "https://[0:0:0:0:0:ffff:a01:219]:8000/v1/operation?theThing=something"; + String expectedIp = "10.1.2.25"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedIp; + NetUtils.getHostByName(expectedIp); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = url; + }}; + + checker.verify(url); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*IP is restricted.*") + public void shouldRejectNAT64HostReferences() throws Exception { + String url = "https://[64:ff9b::]:8000/v1/operation?theThing=something"; + String expectedIp = "64:ff9b:0:0:0:0:0:0"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedIp; + NetUtils.getHostByName(expectedIp); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = url; + NetUtils.isNAT64Address(withInstanceOf(InetAddress.class)); times = 1; result = true; + }}; + + checker.verify(url); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*IP is restricted.*") + public void shouldRejectDecimalIPsThatResolveToLocalIPs() throws Exception { + String url = "https://3232235778:8000/api/v3/pet/findByStatus?status=available"; + String expectedIp = "192.168.1.2"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedIp; + NetUtils.getHostByName(expectedIp); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = url; + }}; + + checker.verify(url); + } + + @Test + public void shouldPreferAllowlistOverEverythingElse() throws Exception { + String url = "https://localhost:3000/1"; + String expectedHostname = "localhost"; + List allowlist = Collections.singletonList("localhost"); + this.checker = new PermittedUrlsChecker(allowlist, emptyDenylist); + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHostname; + }}; + + ResolvedUrl result = checker.verify(url); + + Assert.assertEquals(result.getUrl(), "https://localhost:3000/1"); + Assert.assertEquals(result.getHostHeader(), "localhost"); + } + + @Test + public void shouldAllowPublicDomainsByDefault() throws Exception { + String url = "https://smartbear.com:3000/1"; + String expectedUrl = "https://1.2.3.4:3000/1"; + String expectedHost = "smartbear.com"; + String expectedIp = "1.2.3.4"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHost; + NetUtils.getHostByName(expectedHost); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = expectedUrl; + }}; + + this.checker = new PermittedUrlsChecker(emptyAllowlist, emptyDenylist); + ResolvedUrl result = checker.verify(url); + + Assert.assertEquals(result.getUrl(), expectedUrl); + Assert.assertEquals(result.getHostHeader(), expectedHost); + } + + @Test + public void shouldAllowPublicIPsByDefault() throws Exception { + String url = "https://1.2.3.4:3000/1"; + String expectedHost = "1.2.3.4"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHost; + NetUtils.getHostByName(expectedHost); times = 1; result = InetAddress.getByName(expectedHost); + NetUtils.setHost(url, expectedHost); times = 1; result = url; + }}; + + this.checker = new PermittedUrlsChecker(emptyAllowlist, emptyDenylist); + ResolvedUrl result = checker.verify(url); + + Assert.assertEquals(result.getUrl(), url); + Assert.assertEquals(result.getHostHeader(), expectedHost); + } + + @Test( + dataProvider = "shouldBlockRestrictedIPv4sByDefault", + expectedExceptions = HostDeniedException.class, + expectedExceptionsMessageRegExp = ".*IP is restricted.*" + ) + public void shouldBlockIPv4Localhost(String url, String expectedIp) throws Exception { + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedIp; + NetUtils.getHostByName(expectedIp); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = url; + }}; + + checker.verify(url); + } + + @DataProvider(name = "shouldBlockRestrictedIPv4sByDefault") + private Object[][] shouldBlockRestrictedIPv4sByDefault() { + return new Object[][]{ + {"https://localhost:3000/1", "127.0.0.1"}, + {"https://127.0.0.1/", "127.0.0.1"}, + {"https://192.168.1.2/", "192.168.1.2"}, + {"https://127.3", "127.0.0.3"} + }; + } + + @Test( + dataProvider = "shouldBlockRestrictedIPv6sByDefault", + expectedExceptions = HostDeniedException.class, + expectedExceptionsMessageRegExp = ".*IP is restricted.*" + ) + public void shouldBlockIPv6Localhost(String url, String expectedIp) throws Exception { + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedIp; + NetUtils.getHostByName(expectedIp); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = url; + NetUtils.isUniqueLocalAddress(withInstanceOf(InetAddress.class)); result = true; + }}; + + checker.verify(url); + } + + @DataProvider(name = "shouldBlockRestrictedIPv6sByDefault") + private Object[][] shouldBlockRestrictedIPv6sByDefault() { + return new Object[][]{ + {"https://[fc00::1]/", "fc00:0:0:0:0:0:0:1"}, + {"https://[fd00:ec2::254]/", "fd00:ec2:0:0:0:0:0:254"} + }; + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*IP is restricted.*") + public void shouldBlockDomainNamesThatResolveToPrivateIPs() throws Exception { + String url = "https://evil.com"; + String expectedUrl = "https://192.168.1.1:3000/1"; + String expectedHost = "evil.com"; + String expectedIp = "192.168.1.1"; + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHost; + NetUtils.getHostByName(expectedHost); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = expectedUrl; + }}; + + this.checker = new PermittedUrlsChecker(emptyAllowlist, emptyDenylist); + checker.verify(url); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*URL is part of the explicit denylist.*") + public void shouldBlockSpecificallyDenylistedURLs() throws Exception { + String url = "https://smartbear.com"; + List denylist = Collections.singletonList("smartbear.com"); + + this.checker = new PermittedUrlsChecker(emptyAllowlist, denylist); + checker.verify(url); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*IP is part of the explicit denylist.*") + public void shouldBlockBasedOnResolvedIP() throws Exception { + String url = "https://smartbear.com"; + String expectedUrl = "https://1.2.3.4:3000/1"; + String expectedHost = "smartbear.com"; + String expectedIp = "1.2.3.4"; + List denylist = Collections.singletonList("1.2.3.4"); + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHost; + NetUtils.getHostByName(expectedHost); times = 1; result = InetAddress.getByName(expectedIp); + NetUtils.setHost(url, expectedIp); times = 1; result = expectedUrl; + }}; + + this.checker = new PermittedUrlsChecker(emptyAllowlist, denylist); + checker.verify(url); + } + + @Test(expectedExceptions = HostDeniedException.class, expectedExceptionsMessageRegExp = ".*URL is part of the explicit denylist.*") + public void shouldBlockURLMatchingWildcardPattern() throws Exception { + String url = "https://foo.example.com"; + String expectedHost = "foo.example.com"; + List denylist = Collections.singletonList("f*.example.com"); + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHost; + }}; + + this.checker = new PermittedUrlsChecker(emptyAllowlist, denylist); + checker.verify(url); + } + + @Test + public void shouldAllowURLMatchingWildcardPattern() throws Exception { + String url = "https://foo.example.com"; + String expectedHost = "foo.example.com"; + List allowlist = Collections.singletonList("f*.example.com"); + + new Expectations() {{ + NetUtils.getHostFromUrl(url); times = 1; result = expectedHost; + }}; + + this.checker = new PermittedUrlsChecker(allowlist, emptyDenylist); + checker.verify(url); + } + +} diff --git a/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/parser/urlresolver/matchers/UrlPatternMatcherTest.java b/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/parser/urlresolver/matchers/UrlPatternMatcherTest.java new file mode 100644 index 0000000000..1ff79bd38a --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/parser/urlresolver/matchers/UrlPatternMatcherTest.java @@ -0,0 +1,129 @@ +package io.swagger.parser.urlresolver.matchers; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.List; + +public class UrlPatternMatcherTest { + + @Test + public void returnsFalseWhenUrlCannotBeParsed() { + List patterns = Collections.emptyList(); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("not a url")); + } + + @Test + public void returnsFalseWhenUrlIsNotHttpOrHttps() { + List patterns = Collections.emptyList(); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("file://not a url")); + } + + @Test + public void domainWithoutPortMatchesAnyPort() { + List patterns = Collections.singletonList("example.com"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertTrue(matcher.matches("http://example.com")); + Assert.assertTrue(matcher.matches("https://example.com")); + Assert.assertTrue(matcher.matches("http://example.com:12345")); + Assert.assertTrue(matcher.matches("https://example.com:12345")); + Assert.assertFalse(matcher.matches("https://not.example.com:12345")); + } + + @Test + public void domainWithPortMatchesOnlyThatPort() { + List patterns = Collections.singletonList("example.com:443"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("http://example.com")); + Assert.assertTrue(matcher.matches("https://example.com")); + Assert.assertTrue(matcher.matches("http://example.com:443")); + Assert.assertFalse(matcher.matches("http://example.com:12345")); + Assert.assertFalse(matcher.matches("https://example.com:1234")); + Assert.assertFalse(matcher.matches("https://not.example.com:12345")); + } + + @Test + public void domainSupportsWildcards() { + List patterns = Collections.singletonList("*.example.com"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("http://example.com")); + Assert.assertFalse(matcher.matches("https://example.com")); + Assert.assertFalse(matcher.matches("https://fooexample.com")); + Assert.assertTrue(matcher.matches("https://foo.example.com")); + Assert.assertTrue(matcher.matches("https://foo.bar.example.com")); + } + + @Test + public void domainInUrlIsCaseInsensitive() { + List patterns = Collections.singletonList("*.example.com"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("http://ExAmPlE.CoM")); + Assert.assertTrue(matcher.matches("https://FoO.ExAmPlE.CoM")); + } + + @Test + public void domainInPatternIsCaseInsensitive() { + List patterns = Collections.singletonList("*.EXamPLe.Com"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("http://ExAmPlE.CoM")); + Assert.assertTrue(matcher.matches("https://FoO.ExAmPlE.CoM")); + } + + @Test + public void supportForMatchingInternationalizedDomainNames() { + List patterns = Collections.singletonList("*.😋.local"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("http://example.com")); + Assert.assertTrue(matcher.matches("http://blah.😋.local")); + Assert.assertTrue(matcher.matches("http://blah.xn--p28h.local")); + } + + @Test + public void domainsDoNotSupportWildcardsAtTheEnd() { + List patterns = Collections.singletonList("example.co*"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("https://example.net")); + Assert.assertFalse(matcher.matches("https://example.co.uk")); + Assert.assertFalse(matcher.matches("https://example.com")); + } + + @Test + public void ipAddressesSupportWildcardsAtTheEnd() { + List patterns = Collections.singletonList("10.100.*.*"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertTrue(matcher.matches("http://10.100.1.2")); + Assert.assertFalse(matcher.matches("http://10.101.1.2")); + } + + @Test + public void worksWithUrlsWithAuthPathAndQueryComponents() { + List patterns = Collections.singletonList("*.example.com"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertFalse(matcher.matches("https://foo:bar@example.com/path?q=1")); + Assert.assertTrue(matcher.matches("https://foo:bar@foo.example.com/path?q=1")); + } + + @Test + public void supportsIpAddressesInPatterns() { + List patterns = Collections.singletonList("1.*.3.4"); + UrlPatternMatcher matcher = new UrlPatternMatcher(patterns); + + Assert.assertTrue(matcher.matches("https://foo:bar@1.2.3.4/path?q=1")); + Assert.assertFalse(matcher.matches("https://foo:bar@1.2.3.5/path?q=1")); + } + +} diff --git a/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/parser/urlresolver/utils/NetUtilsTest.java b/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/parser/urlresolver/utils/NetUtilsTest.java new file mode 100644 index 0000000000..7e7cea3773 --- /dev/null +++ b/modules/swagger-parser-safe-url-resolver/src/test/java/io/swagger/parser/urlresolver/utils/NetUtilsTest.java @@ -0,0 +1,138 @@ +package io.swagger.parser.urlresolver.utils; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.UnknownHostException; + +public class NetUtilsTest { + + @Test + public void getHostFromUrlWithDomainNameShouldReturnHostname() throws MalformedURLException { + String url = "https://example.com/hello?query=world"; + + String hostname = NetUtils.getHostFromUrl(url); + + Assert.assertEquals(hostname, "example.com"); + } + + @Test + public void getHostFromUrlWithIPv4AddressShouldReturnIPAddress() throws MalformedURLException { + String url = "https://1.2.3.4/hello?query=world"; + + String hostname = NetUtils.getHostFromUrl(url); + + Assert.assertEquals(hostname, "1.2.3.4"); + } + + @Test + public void getHostFromUrlWithIPv6AddressShouldReturnIPAddress() throws MalformedURLException { + String url = "https://[::1]/hello?query=world"; + + String hostname = NetUtils.getHostFromUrl(url); + + Assert.assertEquals(hostname, "::1"); + } + + @Test + public void setHostShouldSetIPv4AddressInUrl() throws MalformedURLException { + String url = "https://example.com/hello?query=world"; + String ip = "1.2.3.4"; + + String result = NetUtils.setHost(url, ip); + + Assert.assertEquals(result, "https://1.2.3.4/hello?query=world"); + } + + @Test + public void setHostShouldSetIPv6AddressInUrlWithBrackets() throws MalformedURLException { + String url = "https://example.com/hello?query=world"; + String ip = "::1"; + + String result = NetUtils.setHost(url, ip); + + Assert.assertEquals(result, "https://[::1]/hello?query=world"); + } + + @Test + public void isIPv4WithIPv4AddressShouldReturnTrue() { + String ip = "1.2.3.4"; + + Assert.assertTrue(NetUtils.isIPv4(ip)); + } + + @Test + public void isIPv4WithIPv6AddressShouldReturnFalse() { + String ip = "::1"; + + Assert.assertFalse(NetUtils.isIPv4(ip)); + } + + @Test + public void isIPv6WithIPv6AddressShouldReturnTrue() { + String ip = "::1"; + + Assert.assertTrue(NetUtils.isIPv6(ip)); + } + + @Test + public void isIPv6WithIPv4AddressShouldReturnFalse() { + String ip = "1.2.3.4"; + + Assert.assertFalse(NetUtils.isIPv6(ip)); + } + + @Test + public void isIPv6WithImproperAddressShouldReturnFalse() { + String ip = "999.999.999.999"; + + Assert.assertFalse(NetUtils.isIPv6(ip)); + } + + @Test + public void isUniqueLocalAddressWithULAShouldReturnTrue() throws UnknownHostException { + InetAddress ulaIp = InetAddress.getByName("fc00::1"); + InetAddress ulaIpWithLBit = InetAddress.getByName("fd00:ec2::254"); + + Assert.assertTrue(NetUtils.isUniqueLocalAddress(ulaIp)); + Assert.assertTrue(NetUtils.isUniqueLocalAddress(ulaIpWithLBit)); + } + + @Test + public void isUniqueLocalAddressWithNonULAIPv6AddressShouldReturnFalse() throws UnknownHostException { + InetAddress ip = InetAddress.getByName("::1"); + + Assert.assertFalse(NetUtils.isUniqueLocalAddress(ip)); + } + + @Test + public void isUniqueLocalAddressWithIPv4AddressShouldReturnFalse() throws UnknownHostException { + InetAddress ip = InetAddress.getByName("1.2.3.4"); + + Assert.assertFalse(NetUtils.isUniqueLocalAddress(ip)); + } + + @Test + public void isNAT64WithNAT64AddressShouldReturnTrue() throws UnknownHostException { + InetAddress ip = InetAddress.getByName("64:ff9b::"); + + Assert.assertTrue(NetUtils.isNAT64Address(ip)); + } + + @Test + public void isNAT64WithRegularIPv6AddressShouldReturnFalse() throws UnknownHostException { + InetAddress ip = InetAddress.getByName("fc00::1"); + + Assert.assertFalse(NetUtils.isNAT64Address(ip)); + } + + @Test + public void isNAT64WithIPv4AddressShouldReturnFalse() throws UnknownHostException { + InetAddress ip = InetAddress.getByName("1.2.3.4"); + + Assert.assertFalse(NetUtils.isNAT64Address(ip)); + } + +} diff --git a/modules/swagger-parser/pom.xml b/modules/swagger-parser/pom.xml index a383b3dfc2..f3a122549b 100644 --- a/modules/swagger-parser/pom.xml +++ b/modules/swagger-parser/pom.xml @@ -4,13 +4,14 @@ io.swagger swagger-parser-project - 1.0.40-SNAPSHOT + 1.0.76-SNAPSHOT ../.. 4.0.0 swagger-parser jar swagger-parser + swagger-parser test-single @@ -18,6 +19,7 @@ maven-surefire-plugin + ${surefire-version} single . @@ -28,23 +30,33 @@ + + io.swagger + swagger-core + ${swagger-core-version} + + + org.yaml + snakeyaml + ${snakeyaml-version} + org.testng testng ${testng-version} test + + commons-io + commons-io + ${commons-io-version} + org.jmockit jmockit ${jmockit-version} test - - io.swagger - swagger-core - ${swagger-core-version} - ${project.parent.groupId} swagger-core @@ -58,21 +70,6 @@ ${junit-version} test - - org.slf4j - slf4j-ext - ${slf4j-version} - - - org.slf4j - slf4j-api - ${slf4j-version} - - - commons-io - commons-io - ${commons-io-version} - org.slf4j slf4j-simple @@ -91,5 +88,10 @@ + + io.swagger + swagger-parser-safe-url-resolver + ${project.version} + diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/ResolverCache.java b/modules/swagger-parser/src/main/java/io/swagger/parser/ResolverCache.java index fc4dab93c5..2012fe5de0 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/ResolverCache.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/ResolverCache.java @@ -8,14 +8,14 @@ import io.swagger.models.Response; import io.swagger.models.Swagger; import io.swagger.models.auth.AuthorizationValue; +import io.swagger.models.parameters.BodyParameter; import io.swagger.models.properties.Property; import io.swagger.models.properties.RefProperty; import io.swagger.models.refs.RefFormat; import io.swagger.models.refs.RefType; -import io.swagger.parser.util.DeserializationUtils; -import io.swagger.parser.util.PathUtils; -import io.swagger.parser.util.RefUtils; -import io.swagger.parser.util.SwaggerDeserializer; +import io.swagger.parser.util.*; +import io.swagger.parser.urlresolver.PermittedUrlsChecker; +import io.swagger.parser.urlresolver.exceptions.HostDeniedException; import org.apache.commons.lang3.StringUtils; import java.io.File; @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -51,6 +52,7 @@ public class ResolverCache { private final Path parentDirectory; private final String parentUrl; private final String rootPath; + private final ParseOptions parseOptions; private Map resolutionCache = new HashMap<>(); private Map externalFileCache = new HashMap<>(); private Set referencedModelKeys = new HashSet<>(); @@ -58,12 +60,17 @@ public class ResolverCache { /* a map that stores original external references, and their associated renamed references */ - private Map renameCache = new HashMap<>(); + private Map renameCache = new ConcurrentHashMap<>(); public ResolverCache(Swagger swagger, List auths, String parentFileLocation) { + this(swagger, auths, parentFileLocation, new ParseOptions()); + } + + public ResolverCache(Swagger swagger, List auths, String parentFileLocation, ParseOptions parseOptions) { this.swagger = swagger; this.auths = auths; this.rootPath = parentFileLocation; + this.parseOptions = parseOptions; if(parentFileLocation != null) { if(parentFileLocation.startsWith("http")) { @@ -113,6 +120,10 @@ public T loadRef(String ref, RefFormat refFormat, Class expectedType) { String contents = externalFileCache.get(file); if (contents == null) { + if(parseOptions.isSafelyResolveURL()){ + checkUrlIsPermitted(file); + } + if(parentDirectory != null) { contents = RefUtils.readExternalRef(file, refFormat, auths, parentDirectory); } @@ -129,7 +140,7 @@ else if(rootPath != null) { } //a definition path is defined, meaning we need to "dig down" through the JSON tree and get the desired entity - JsonNode tree = DeserializationUtils.deserializeIntoTree(contents, file); + JsonNode tree = deserialize(contents, file); String[] jsonPathElements = definitionPath.split("/"); for (String jsonPathElement : jsonPathElements) { @@ -151,10 +162,33 @@ else if(rootPath != null) { updateLocalRefs(file, result); resolutionCache.put(ref, result); + + if (result instanceof BodyParameter) { + loadRef(ref, refFormat, (BodyParameter) result); + } return result; } + private void loadRef(String ref, RefFormat refFormat, final BodyParameter bodyParameter) { + final Model schema = bodyParameter.getSchema(); + if (schema instanceof RefModel && refFormat != RefFormat.INTERNAL) { + loadRef(ref, refFormat, (RefModel) schema); + } + } + + private void loadRef(String ref, RefFormat refFormat, final RefModel refModel) { + final String rootRef = ref.substring(0, ref.indexOf('#')); + final String externalRef = RefUtils.isAnExternalRefFormat(refModel.getRefFormat()) ? refModel.getReference() + : rootRef + refModel.getReference(); + final Model derefModel = loadRef(externalRef, refFormat, Model.class); + swagger.addDefinition(refModel.getSimpleRef(), derefModel); + } + + protected JsonNode deserialize(String contents, String file) { + return DeserializationUtils.deserializeIntoTree(contents, file); + } + protected void updateLocalRefs(String file, T result) { if(result instanceof Response) { Response response = (Response) result; @@ -266,6 +300,17 @@ private Object getFromMap(String ref, Map map, Pattern pattern) { return null; } + protected void checkUrlIsPermitted(String refSet) { + try { + PermittedUrlsChecker permittedUrlsChecker = new PermittedUrlsChecker(parseOptions.getRemoteRefAllowList(), + parseOptions.getRemoteRefBlockList()); + + permittedUrlsChecker.verify(refSet); + } catch (HostDeniedException exception) { + throw new RuntimeException(exception.getMessage()); + } + } + public boolean hasReferencedKey(String modelKey) { if(referencedModelKeys == null) { return false; diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/Swagger20Parser.java b/modules/swagger-parser/src/main/java/io/swagger/parser/Swagger20Parser.java index b5b845565d..475291cf78 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/Swagger20Parser.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/Swagger20Parser.java @@ -37,7 +37,7 @@ public SwaggerDeserializationResult readWithInfo(JsonNode node) { @Override public SwaggerDeserializationResult readWithInfo(String location, List auths) { String data; - + SwaggerDeserializationResult errorOutput = new SwaggerDeserializationResult(); try { location = location.replaceAll("\\\\","/"); if (location.toLowerCase().startsWith("http")) { @@ -61,24 +61,30 @@ public SwaggerDeserializationResult readWithInfo(String location, List auths) throws IOException { LOGGER.info("reading from " + location); @@ -118,7 +124,7 @@ private Swagger convertToSwagger(String data) throws IOException { ObjectMapper mapper = Json.mapper(); rootNode = mapper.readTree(data); } else { - rootNode = DeserializationUtils.readYamlTree(data); + rootNode = deserializeYaml(data); } if (System.getProperty("debugParser") != null) { @@ -157,7 +163,17 @@ public Swagger read(JsonNode node) throws IOException { if (node == null) { return null; } - - return Json.mapper().convertValue(node, Swagger.class); + try { + // try first core deserializer, to ensure unchanged behaviour for working specs + return Json.mapper().convertValue(node, Swagger.class); + } catch (Exception e) { + LOGGER.error("Exception deserializing via core Json.mapper(), trying parser deserialization"); + SwaggerDeserializationResult result = (new SwaggerDeserializer()).deserialize(node); + Swagger convertValue = result.getSwagger(); + if (System.getProperty("debugParser") != null) { + LOGGER.info("\n\nSwagger Tree convertValue : \n" + ReflectionToStringBuilder.toString(convertValue, ToStringStyle.MULTI_LINE_STYLE) + "\n\n"); + } + return convertValue; + } } } diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerParser.java b/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerParser.java index 5041c0cfb2..dd7f1afe9b 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerParser.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerParser.java @@ -5,6 +5,8 @@ import io.swagger.models.Swagger; import io.swagger.models.auth.AuthorizationValue; import io.swagger.parser.util.DeserializationUtils; +import io.swagger.parser.util.InlineModelResolver; +import io.swagger.parser.util.ParseOptions; import io.swagger.parser.util.SwaggerDeserializationResult; import io.swagger.util.Json; @@ -24,6 +26,7 @@ public SwaggerDeserializationResult readWithInfo(String location, List parserExtensions = getExtensions(); SwaggerDeserializationResult output = new SwaggerDeserializationResult(); + List results = new ArrayList<>(); try { if (auths == null) { auths = new ArrayList(); @@ -43,14 +46,22 @@ public SwaggerDeserializationResult readWithInfo(String location, List auths, boolean resolve) { + ParseOptions options = new ParseOptions(); + options.setResolve(resolve); + return read(location, auths, options); + } + + public Swagger read(String location, List auths, ParseOptions options) { if (location == null) { return null; } @@ -68,7 +85,16 @@ public Swagger read(String location, List auths, boolean res try { output = new Swagger20Parser().read(location, auths); if (output != null) { - return new SwaggerResolver(output, auths, location).resolve(); + if (options != null) { + if (options.isResolve()) { + output = new SwaggerResolver(output, auths, location, null, options).resolve(); + } + if (options.isFlatten()) { + InlineModelResolver inlineModelResolver = new InlineModelResolver(); + inlineModelResolver.flatten(output); + } + } + return output; } } catch (IOException e) { } @@ -94,6 +120,10 @@ public SwaggerDeserializationResult readWithInfo(String swaggerAsString) { return readWithInfo(swaggerAsString, Boolean.TRUE); } + protected JsonNode deserializeYaml(String data) throws IOException{ + return DeserializationUtils.readYamlTree(data, null); + } + public SwaggerDeserializationResult readWithInfo(String swaggerAsString, boolean resolve) { if (swaggerAsString == null || swaggerAsString.trim().isEmpty()) { return new SwaggerDeserializationResult().message("empty or null swagger supplied"); @@ -104,7 +134,7 @@ public SwaggerDeserializationResult readWithInfo(String swaggerAsString, boolean ObjectMapper mapper = Json.mapper(); node = mapper.readTree(swaggerAsString); } else { - node = DeserializationUtils.readYamlTree(swaggerAsString); + node = deserializeYaml(swaggerAsString); } SwaggerDeserializationResult result = new Swagger20Parser().readWithInfo(node); @@ -138,14 +168,20 @@ public Swagger parse(String swaggerAsString, List auths) { } public Swagger read(JsonNode node) { - return read(node, new ArrayList(), false); + return read(node, false); } public Swagger read(JsonNode node, boolean resolve) { - return read(node, new ArrayList(), resolve); + return read(node, new ArrayList<>(), resolve); } public Swagger read(JsonNode node, List authorizationValues, boolean resolve) { + ParseOptions options = new ParseOptions(); + options.setResolve(resolve); + return read(node, authorizationValues, options); + } + + public Swagger read(JsonNode node, List authorizationValues, ParseOptions options) { if (node == null) { return null; } @@ -156,11 +192,16 @@ public Swagger read(JsonNode node, List authorizationValues, try { output = new Swagger20Parser().read(node); if (output != null) { - if (resolve) { - return new SwaggerResolver(output, authorizationValues).resolve(); - } else { - return output; + if (options != null) { + if (options.isResolve()) { + output = new SwaggerResolver(output, authorizationValues, null, null, options).resolve(); + } + if (options.isFlatten()) { + InlineModelResolver inlineModelResolver = new InlineModelResolver(); + inlineModelResolver.flatten(output); + } } + return output; } } catch (IOException e) { } @@ -189,4 +230,4 @@ public List getExtensions() { } return output; } -} \ No newline at end of file +} diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerResolver.java b/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerResolver.java index 32ab5278eb..536339ff98 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerResolver.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/SwaggerResolver.java @@ -4,10 +4,16 @@ import io.swagger.models.Path; import io.swagger.models.Swagger; import io.swagger.models.auth.AuthorizationValue; +import io.swagger.models.parameters.Parameter; +import io.swagger.models.parameters.RefParameter; +import io.swagger.models.refs.RefFormat; import io.swagger.parser.processors.DefinitionsProcessor; import io.swagger.parser.processors.OperationProcessor; +import io.swagger.parser.processors.ParameterProcessor; import io.swagger.parser.processors.PathsProcessor; +import io.swagger.parser.util.ParseOptions; +import java.util.Arrays; import java.util.List; /** @@ -19,6 +25,7 @@ public class SwaggerResolver { private final PathsProcessor pathProcessor; private final DefinitionsProcessor definitionsProcessor; private final OperationProcessor operationsProcessor; + private final ParameterProcessor parametersProcessor; private Settings settings = new Settings(); public SwaggerResolver(Swagger swagger) { @@ -34,12 +41,17 @@ public SwaggerResolver(Swagger swagger, List auths, String p } public SwaggerResolver(Swagger swagger, List auths, String parentFileLocation, Settings settings) { + this(swagger, auths, parentFileLocation, settings, new ParseOptions()); + } + + public SwaggerResolver(Swagger swagger, List auths, String parentFileLocation, Settings settings, ParseOptions parseOptions) { this.swagger = swagger; this.settings = settings != null ? settings : new Settings(); - this.cache = new ResolverCache(swagger, auths, parentFileLocation); + this.cache = new ResolverCache(swagger, auths, parentFileLocation, parseOptions); definitionsProcessor = new DefinitionsProcessor(cache, swagger); pathProcessor = new PathsProcessor(cache, swagger, this.settings); operationsProcessor = new OperationProcessor(cache, swagger); + parametersProcessor = new ParameterProcessor(cache, swagger); } public Swagger resolve() { @@ -47,6 +59,15 @@ public Swagger resolve() { return null; } + if (swagger.getParameters() != null) { + for(String paramname : swagger.getParameters().keySet()) { + Parameter param = swagger.getParameters().get(paramname); + if (param instanceof RefParameter && ((RefParameter) param).getRefFormat() == RefFormat.RELATIVE) { + swagger.getParameters().put(paramname, parametersProcessor.processParameters(Arrays.asList(param)).get(0)); + } + } + } + pathProcessor.processPaths(); definitionsProcessor.processDefinitions(); diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/processors/ExternalRefProcessor.java b/modules/swagger-parser/src/main/java/io/swagger/parser/processors/ExternalRefProcessor.java index 5a6c94d9b8..23d5df9b03 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/processors/ExternalRefProcessor.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/processors/ExternalRefProcessor.java @@ -2,20 +2,25 @@ import io.swagger.models.*; import io.swagger.models.properties.ArrayProperty; +import io.swagger.models.properties.ComposedProperty; import io.swagger.models.properties.MapProperty; import io.swagger.models.properties.ObjectProperty; import io.swagger.models.properties.Property; import io.swagger.models.properties.RefProperty; +import io.swagger.models.properties.StringProperty; import io.swagger.models.refs.RefFormat; +import io.swagger.models.refs.RefType; import io.swagger.parser.ResolverCache; -import io.swagger.parser.util.RefUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.LoggerFactory; import java.net.URI; +import java.util.Arrays; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import static io.swagger.parser.util.RefUtils.computeDefinitionName; @@ -109,10 +114,7 @@ public String processRefToExternalDefinition(String $ref, RefFormat refFormat) { if (isAnExternalRefFormat(refModel.getRefFormat())) { String joinedRef = join(file, refModel.get$ref()); refModel.set$ref(processRefToExternalDefinition(joinedRef, refModel.getRefFormat())); - }/*else if (isAnExternalRefFormat(refModel.getOriginalRefFormat())) { - String joinedRef = join(file, refModel.getOriginalRef()); - refModel.set$ref(processRefToExternalDefinition(joinedRef, refModel.getOriginalRefFormat())); - }*/else { + }else { processRefToExternalDefinition(file + refModel.get$ref(), RefFormat.RELATIVE); } } else if (allOfModel instanceof ModelImpl) { @@ -126,36 +128,37 @@ public String processRefToExternalDefinition(String $ref, RefFormat refFormat) { if (model instanceof ModelImpl) { ModelImpl modelImpl = (ModelImpl) model; - Property additionalProperties = modelImpl.getAdditionalProperties(); - if (additionalProperties != null) { - if (additionalProperties instanceof RefProperty) { - processRefProperty(((RefProperty) additionalProperties), file); - } else if (additionalProperties instanceof ArrayProperty) { - ArrayProperty arrayProp = (ArrayProperty) additionalProperties; - if (arrayProp.getItems() instanceof RefProperty) { - processRefProperty((RefProperty) arrayProp.getItems(), file); - } - } else if (additionalProperties instanceof MapProperty) { - MapProperty mapProp = (MapProperty) additionalProperties; - if (mapProp.getAdditionalProperties() instanceof RefProperty) { - processRefProperty((RefProperty) mapProp.getAdditionalProperties(), file); - } else if (mapProp.getAdditionalProperties() instanceof ArrayProperty && - ((ArrayProperty) mapProp.getAdditionalProperties()).getItems() instanceof RefProperty) { - processRefProperty((RefProperty) ((ArrayProperty) mapProp.getAdditionalProperties()).getItems(), file); - } - } + String discriminator = modelImpl.getDiscriminator(); + if (discriminator != null){ + processDiscriminator(discriminator,modelImpl.getProperties(), file); } + + processProperties(Arrays.asList(modelImpl.getAdditionalProperties()), file); } if (model instanceof ArrayModel && ((ArrayModel) model).getItems() instanceof RefProperty) { processRefProperty((RefProperty) ((ArrayModel) model).getItems(), file); } + + if (model instanceof ArrayModel && ((ArrayModel) model).getItems() != null) { + ArrayModel arraySchema = (ArrayModel) model; + if (arraySchema.getItems() instanceof RefModel) { + processRefProperty((RefProperty) ((ArrayModel) model).getItems(), file); + } else { + Property properties = arraySchema.getItems(); + if (properties instanceof ObjectProperty) { + processProperties(((ObjectProperty) properties).getProperties(), file); + } + } + } } return newRef; } + + public String processRefToExternalResponse(String $ref, RefFormat refFormat) { String renamedRef = cache.getRenamedRef($ref); @@ -189,75 +192,196 @@ public String processRefToExternalResponse(String $ref, RefFormat refFormat) { if(response != null) { - String file = $ref.split("#/")[0]; - Model model = null; if (response.getResponseSchema() != null) { - model = response.getResponseSchema(); - if (model instanceof RefModel) { - RefModel refModel = (RefModel) model; - if (RefUtils.isAnExternalRefFormat(refFormat)) { - processRefModel(refModel, $ref); - } else { - processRefToExternalDefinition(file + refModel.get$ref(), RefFormat.RELATIVE); - } - } + processRefSchemaObject(response.getResponseSchema(), $ref); } } return newRef; } - private void processProperties(final Map subProps, final String file) { - if (subProps == null || 0 == subProps.entrySet().size() ) { - return; + private void processRefSchemaObject(Model schema, String $ref) { + String file = $ref.split("#/")[0]; + + if (schema instanceof RefModel) { + RefModel refModel = (RefModel) schema; + RefFormat ref = refModel.getRefFormat(); + if (isAnExternalRefFormat(ref)) { + processRefModel(refModel, $ref); + } else { + processRefToExternalDefinition(file + refModel.get$ref(), RefFormat.RELATIVE); + } + }else{ + processSchema(schema,file); } - for (Map.Entry prop : subProps.entrySet()) { - if (prop.getValue() instanceof RefProperty) { - processRefProperty((RefProperty) prop.getValue(), file); - } else if (prop.getValue() instanceof ArrayProperty) { - ArrayProperty arrayProp = (ArrayProperty) prop.getValue(); - if (arrayProp.getItems() instanceof RefProperty) { - processRefProperty((RefProperty) arrayProp.getItems(), file); + } + + private void processSchema(Model property, String file) { + if (property != null) { + if (property instanceof RefModel) { + processRefModel((RefModel)property, file); + } + if (property.getProperties() != null) { + processProperties(property.getProperties(), file); + } + if (property instanceof ArrayModel) { + processProperty(((ArrayModel) property).getItems(), file); + } + if (property instanceof MapProperty){ + MapProperty mapProperty = (MapProperty) property; + if (mapProperty.getAdditionalProperties() instanceof Model) { + processProperty(mapProperty.getAdditionalProperties(), file); } - if (arrayProp.getItems() != null){ - if (arrayProp.getItems() instanceof ObjectProperty) { - ObjectProperty objectProperty = (ObjectProperty) arrayProp.getItems(); - processProperties(objectProperty.getProperties(), file); + } + if (property instanceof ComposedModel) { + ComposedModel composed = (ComposedModel) property; + processComposedProperties(composed.getAllOf(), file); + } + } + } + + private void processProperty(Property property, String file) { + } + + private void processComposedProperties(Collection properties, String file) { + if (properties != null) { + for (Model property : properties) { + processSchema(property, file); + } + } + } + + + private void processDiscriminator(String discriminator, Map properties, String file) { + if (properties == null || properties.isEmpty()) { + return; + } + for (Map.Entry prop : properties.entrySet()) { + if (prop.getKey().equals(discriminator)){ + if (prop.getValue() instanceof StringProperty){ + StringProperty stringProperty = (StringProperty) prop.getValue(); + if(stringProperty.getEnum() != null){ + for(String name: stringProperty.getEnum()){ + processRefProperty(new RefProperty(RefType.DEFINITION.getInternalPrefix()+name), file); + } + } + }else if (prop.getValue() instanceof RefProperty) { + String ref = ((RefProperty) prop.getValue()).getSimpleRef(); + Map renameCache = cache.getRenameCache(); + for (String key : renameCache.keySet()) { + String value = renameCache.get(key); + if (value.equals(ref)) { + Object resolved = cache.getResolutionCache().get(key); + if(resolved != null) { + if (resolved instanceof ModelImpl) { + ModelImpl schema = (ModelImpl) resolved; + if (schema.getEnum() != null) { + for (String name : schema.getEnum()) { + processRefProperty(new RefProperty(RefType.DEFINITION.getInternalPrefix() + name), file); + } + } + } + } + } } - } - } else if (prop.getValue() instanceof MapProperty) { - MapProperty mapProp = (MapProperty) prop.getValue(); - if (mapProp.getAdditionalProperties() instanceof RefProperty) { - processRefProperty((RefProperty) mapProp.getAdditionalProperties(), file); - } else if (mapProp.getAdditionalProperties() instanceof ArrayProperty && - ((ArrayProperty) mapProp.getAdditionalProperties()).getItems() instanceof RefProperty) { - processRefProperty((RefProperty) ((ArrayProperty) mapProp.getAdditionalProperties()).getItems(), - file); } } } + } - private void processRefProperty(RefProperty subRef, String externalFile) { + private void processProperties(final Map subProps, final String file) { + if (subProps == null || subProps.isEmpty()) { + return; + } + processProperties(subProps.values(), file); + } + + private void processProperties(final Collection subProps, final String file) { + if (subProps == null || subProps.isEmpty()) { + return; + } + for (Property prop : subProps) { + if (prop instanceof RefProperty) { + processRefProperty((RefProperty) prop, file); + } else if (prop instanceof ArrayProperty) { + processProperties(Arrays.asList(((ArrayProperty) prop).getItems()), file); + } else if (prop instanceof MapProperty) { + processProperties(Arrays.asList(((MapProperty) prop).getAdditionalProperties()), file); + } else if (prop instanceof ObjectProperty) { + processProperties(((ObjectProperty) prop).getProperties(), file); + } else if (prop instanceof ComposedProperty) { + processProperties(((ComposedProperty) prop).getAllOf(), file); + } + } + } + + private void processDiscriminatorAsRefProperty(RefProperty subRef, String externalFile) { if (isAnExternalRefFormat(subRef.getRefFormat())) { String joinedRef = join(externalFile, subRef.get$ref()); subRef.set$ref(processRefToExternalDefinition(joinedRef, subRef.getRefFormat())); } else { - subRef.set$ref(processRefToExternalDefinition(externalFile + subRef.get$ref(), RefFormat.RELATIVE)); + String processRef = processRefToExternalDefinition(externalFile + subRef.get$ref(), RefFormat.RELATIVE); + subRef.set$ref(RefType.DEFINITION.getInternalPrefix()+processRef); } } - private void processRefModel(RefModel subRef, String externalFile) { + private void processRefProperty(RefProperty subRef, String externalFile) { if (isAnExternalRefFormat(subRef.getRefFormat())) { + String joinedRef = join(externalFile, subRef.get$ref()); - subRef.set$ref(processRefToExternalDefinition(joinedRef, subRef.getRefFormat())); + String processRef = processRefToExternalDefinition(joinedRef, subRef.getRefFormat()); + if(processRef.startsWith("http") || processRef.startsWith("https:")) { + subRef.set$ref(processRef); + }else { + subRef.set$ref(RefType.DEFINITION.getInternalPrefix()+processRef); + } } else { - processRefToExternalDefinition(externalFile + subRef.get$ref(), RefFormat.RELATIVE); + String processRef = processRefToExternalDefinition(externalFile + subRef.get$ref(), RefFormat.RELATIVE); + subRef.set$ref(RefType.DEFINITION.getInternalPrefix()+processRef); + } + } + + private void processRefModel(RefModel subRef, String externalFile) { + + RefFormat format = subRef.getRefFormat(); + + if (!isAnExternalRefFormat(format)) { + subRef.set$ref(RefType.DEFINITION.getInternalPrefix()+ processRefToExternalDefinition(externalFile + subRef.get$ref(), RefFormat.RELATIVE)); + return; + } + String $ref = subRef.get$ref(); + String subRefExternalPath = getExternalPath(subRef.get$ref()); + + if (format.equals(RefFormat.RELATIVE) && !Objects.equals(subRefExternalPath, externalFile)) { + $ref = constructRef(subRef, externalFile); + subRef.set$ref($ref); + }else { + processRefToExternalDefinition($ref, format); + } + } + + protected String constructRef(Model refProperty, String rootLocation) { + RefModel refModel = (RefModel) refProperty; + String ref = refModel.get$ref(); + return join(rootLocation, ref); + } + + public static String getExternalPath(String ref) { + if (ref == null) { + return null; + } + String[] elements = ref.split("#/"); + String element = null; + for (int i = 0; i < elements.length; i++ ) { + if (elements[i].length() == 2){ + element = elements[i]; + } } + return element; } - public static String join(String source, String fragment) { try { diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/processors/ModelProcessor.java b/modules/swagger-parser/src/main/java/io/swagger/parser/processors/ModelProcessor.java index 2bcca6726b..6f93c76d67 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/processors/ModelProcessor.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/processors/ModelProcessor.java @@ -7,6 +7,7 @@ import io.swagger.models.RefModel; import io.swagger.models.Swagger; import io.swagger.models.properties.Property; +import io.swagger.models.refs.RefType; import io.swagger.parser.ResolverCache; import java.util.List; @@ -36,13 +37,13 @@ public void processModel(Model model) { } else if (model instanceof ComposedModel) { processComposedModel((ComposedModel) model); } else if (model instanceof ModelImpl) { - processModelImpl((ModelImpl) model); + processModelProperties( model); } } - private void processModelImpl(ModelImpl modelImpl) { + private void processModelProperties(Model model) { - final Map properties = modelImpl.getProperties(); + final Map properties = model.getProperties(); if (properties == null) { return; @@ -67,6 +68,7 @@ private void processComposedModel(ComposedModel composedModel) { } } + processModelProperties(composedModel); } private void processArrayModel(ArrayModel arrayModel) { @@ -96,7 +98,8 @@ private void processRefModel(RefModel refModel) { } if (newRef != null) { - refModel.set$ref(newRef); + refModel.set$ref(RefType.DEFINITION.getInternalPrefix() + newRef); + } } diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/processors/OperationProcessor.java b/modules/swagger-parser/src/main/java/io/swagger/parser/processors/OperationProcessor.java index 073afdbd0e..eaf2a262a2 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/processors/OperationProcessor.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/processors/OperationProcessor.java @@ -3,6 +3,7 @@ import io.swagger.models.Operation; import io.swagger.models.RefResponse; import io.swagger.models.Response; +import io.swagger.models.Responses; import io.swagger.models.Swagger; import io.swagger.models.parameters.Parameter; import io.swagger.parser.ResolverCache; @@ -26,7 +27,7 @@ public void processOperation(Operation operation) { final List processedOperationParameters = parameterProcessor.processParameters(operation.getParameters()); operation.setParameters(processedOperationParameters); - final Map responses = operation.getResponses(); + final Responses responses = operation.getResponsesObject(); if (responses != null) { for (String responseCode : responses.keySet()) { diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/processors/PathsProcessor.java b/modules/swagger-parser/src/main/java/io/swagger/parser/processors/PathsProcessor.java index 6e37fe8f6d..f6484784ca 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/processors/PathsProcessor.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/processors/PathsProcessor.java @@ -5,6 +5,7 @@ import io.swagger.models.parameters.Parameter; import io.swagger.models.properties.Property; import io.swagger.models.properties.RefProperty; +import io.swagger.models.refs.RefFormat; import io.swagger.parser.ResolverCache; import io.swagger.parser.SwaggerResolver; @@ -118,8 +119,8 @@ protected void updateLocalRefs(Path path, String pathRef) { updateLocalRefs(param, pathRef); } } - if(op.getResponses() != null) { - for(Response response : op.getResponses().values()) { + if(op.getResponsesObject() != null) { + for(Response response : op.getResponsesObject().values()) { updateLocalRefs(response, pathRef); } } @@ -143,12 +144,12 @@ protected void updateLocalRefs(Parameter param, String pathRef) { protected void updateLocalRefs(Model model, String pathRef) { if(model instanceof RefModel) { - RefModel refModel = (RefModel) model; - if(isLocalRef(refModel.get$ref())) { - refModel.set$ref(computeLocalRef(refModel.get$ref(), pathRef)); - }/*else if(isLocalRef(refModel.getOriginalRef())) { - refModel.set$ref(computeLocalRef(refModel.getOriginalRef(), pathRef)); - }*/ + RefModel ref = (RefModel) model; + if(ref.getRefFormat() == RefFormat.INTERNAL) { + ref.set$ref(computeLocalRef(ref.get$ref(), pathRef)); + } else if (ref.getRefFormat() == RefFormat.RELATIVE) { + ref.set$ref(computeRelativeRef(ref.get$ref(), pathRef)); + } } else if(model instanceof ModelImpl) { // process properties @@ -176,22 +177,23 @@ else if(model instanceof ArrayModel) { protected void updateLocalRefs(Property property, String pathRef) { if(property instanceof RefProperty) { RefProperty ref = (RefProperty) property; - if(isLocalRef(ref.get$ref())) { + if(ref.getRefFormat() == RefFormat.INTERNAL) { ref.set$ref(computeLocalRef(ref.get$ref(), pathRef)); - }/*else if(isLocalRef(ref.getOriginalRef())) { - ref.set$ref(computeLocalRef(ref.getOriginalRef(), pathRef)); - }*/ - } - } - - protected boolean isLocalRef(String ref) { - if(ref.startsWith("#")) { - return true; + } else if (ref.getRefFormat() == RefFormat.RELATIVE) { + ref.set$ref(computeRelativeRef(ref.get$ref(), pathRef)); + } } - return false; } protected String computeLocalRef(String ref, String prefix) { return prefix + ref; } + + protected String computeRelativeRef(String ref, String prefix) { + int index = prefix.lastIndexOf('/'); + if (index > 1) { + return prefix.substring(0, index + 1) + ref; + } + return ref; + } } diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/processors/PropertyProcessor.java b/modules/swagger-parser/src/main/java/io/swagger/parser/processors/PropertyProcessor.java index 8da518d86c..1c06b764ac 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/processors/PropertyProcessor.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/processors/PropertyProcessor.java @@ -2,8 +2,11 @@ import io.swagger.models.Swagger; import io.swagger.models.properties.*; +import io.swagger.models.refs.RefFormat; +import io.swagger.models.refs.RefType; import io.swagger.parser.ResolverCache; +import java.util.List; import java.util.Map; import static io.swagger.parser.util.RefUtils.isAnExternalRefFormat; @@ -25,6 +28,8 @@ public void processProperty(Property property) { processMapProperty((MapProperty) property); } else if (property instanceof ObjectProperty) { processObjectProperty((ObjectProperty) property); + } else if (property instanceof ComposedProperty) { + processComposedProperty((ComposedProperty) property); } } @@ -34,7 +39,7 @@ private void processRefProperty(RefProperty refProperty) { final String newRef = externalRefProcessor.processRefToExternalDefinition(refProperty.get$ref(), refProperty.getRefFormat()); if (newRef != null) { - refProperty.set$ref(newRef); + refProperty.set$ref(RefType.DEFINITION.getInternalPrefix()+newRef); } } } @@ -59,4 +64,11 @@ private void processObjectProperty(ObjectProperty property) { for (Property p : properties.values()) processProperty(p); } + + private void processComposedProperty(ComposedProperty property) { + final List properties = property.getAllOf(); + if (properties != null) + for (Property p : properties) + processProperty(p); + } } diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/util/DeserializationUtils.java b/modules/swagger-parser/src/main/java/io/swagger/parser/util/DeserializationUtils.java index dd665edb41..07a61439db 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/util/DeserializationUtils.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/util/DeserializationUtils.java @@ -2,27 +2,165 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import io.swagger.models.Swagger; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import io.swagger.util.Json; +import io.swagger.util.ObjectMapperFactory; import io.swagger.util.Yaml; -import org.apache.commons.lang3.builder.ReflectionToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.constructor.BaseConstructor; import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; +import org.yaml.snakeyaml.resolver.Resolver; import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; -/** - * Created by russellb337 on 7/14/15. - */ public class DeserializationUtils { + + public static class Options { + private Integer maxYamlDepth = System.getProperty("maxYamlDepth") == null ? 2000 : Integer.parseInt(System.getProperty("maxYamlDepth")); + private Long maxYamlReferences = System.getProperty("maxYamlReferences") == null ? 10000000L : Long.parseLong(System.getProperty("maxYamlReferences")); + private boolean validateYamlInput = System.getProperty("validateYamlInput") == null || Boolean.parseBoolean(System.getProperty("validateYamlInput")); + private boolean supportYamlAnchors = System.getProperty("supportYamlAnchors") == null || Boolean.parseBoolean(System.getProperty("supportYamlAnchors")); + private boolean yamlCycleCheck = System.getProperty("yamlCycleCheck") == null || Boolean.parseBoolean(System.getProperty("yamlCycleCheck")); + private Integer maxYamlAliasesForCollections = System.getProperty("maxYamlAliasesForCollections") == null ? Integer.MAX_VALUE : Integer.parseInt(System.getProperty("maxYamlAliasesForCollections")); + private boolean yamlAllowRecursiveKeys = System.getProperty("yamlAllowRecursiveKeys") == null || Boolean.parseBoolean(System.getProperty("yamlAllowRecursiveKeys")); + private Integer maxYamlCodePoints = System.getProperty("maxYamlCodePoints") == null ? 3 * 1024 * 1024 : Integer.parseInt(System.getProperty("maxYamlCodePoints")); + + public Integer getMaxYamlDepth() { + return maxYamlDepth; + } + + public void setMaxYamlDepth(Integer maxYamlDepth) { + this.maxYamlDepth = maxYamlDepth; + } + + public Long getMaxYamlReferences() { + return maxYamlReferences; + } + + public void setMaxYamlReferences(Long maxYamlReferences) { + this.maxYamlReferences = maxYamlReferences; + } + + public boolean isValidateYamlInput() { + return validateYamlInput; + } + + public void setValidateYamlInput(boolean validateYamlInput) { + this.validateYamlInput = validateYamlInput; + } + + public boolean isSupportYamlAnchors() { + return supportYamlAnchors; + } + + public void setSupportYamlAnchors(boolean supportYamlAnchors) { + this.supportYamlAnchors = supportYamlAnchors; + } + + public boolean isYamlCycleCheck() { + return yamlCycleCheck; + } + + public void setYamlCycleCheck(boolean yamlCycleCheck) { + this.yamlCycleCheck = yamlCycleCheck; + } + + /** + * @since 1.0.52 + */ + public Integer getMaxYamlAliasesForCollections() { + return maxYamlAliasesForCollections; + } + + /** + * @since 1.0.52 + */ + public void setMaxYamlAliasesForCollections(Integer maxYamlAliasesForCollections) { + this.maxYamlAliasesForCollections = maxYamlAliasesForCollections; + } + + /** + * @since 1.0.52 + */ + public boolean isYamlAllowRecursiveKeys() { + return yamlAllowRecursiveKeys; + } + + /** + * @since 1.0.52 + */ + public void setYamlAllowRecursiveKeys(boolean yamlAllowRecursiveKeys) { + this.yamlAllowRecursiveKeys = yamlAllowRecursiveKeys; + } + + public Integer getMaxYamlCodePoints() { + return maxYamlCodePoints; + } + + public void setMaxYamlCodePoints(Integer maxYamlCodePointsInBytes) { + this.maxYamlCodePoints = maxYamlCodePointsInBytes; + } + } + + private static Options options = new Options(); + + private static final Logger LOGGER = LoggerFactory.getLogger(DeserializationUtils.class); + + private static ObjectMapper yamlMapper = Yaml.mapper(); + + public static void setYamlMapper(YAMLFactory yamlFactory) { + yamlMapper = ObjectMapperFactory.createYaml(yamlFactory); + } + + public static ObjectMapper getYamlMapper() { + return yamlMapper; + } + + public static Options getOptions() { + return options; + } + public static class CustomResolver extends Resolver { + + /* + * do not resolve timestamp + */ + @Override + protected void addImplicitResolvers() { + addImplicitResolver(Tag.BOOL, BOOL, "yYnNtTfFoO"); + addImplicitResolver(Tag.INT, INT, "-+0123456789"); + addImplicitResolver(Tag.FLOAT, FLOAT, "-+0123456789."); + addImplicitResolver(Tag.MERGE, MERGE, "<"); + addImplicitResolver(Tag.NULL, NULL, "~nN\0"); + addImplicitResolver(Tag.NULL, EMPTY, null); + // addImplicitResolver(Tag.TIMESTAMP, TIMESTAMP, "0123456789"); + } + } + public static JsonNode deserializeIntoTree(String contents, String fileOrHost) { + return deserializeIntoTree(contents, fileOrHost, null); + } + + public static JsonNode deserializeIntoTree(String contents, String fileOrHost, SwaggerDeserializationResult errorOutput) { JsonNode result; try { if (isJson(contents)) { result = Json.mapper().readTree(contents); } else { - result = readYamlTree(contents); + result = readYamlTree(contents, errorOutput); } } catch (IOException e) { throw new RuntimeException("An exception was thrown while trying to deserialize the contents of " + fileOrHost + " into a JsonNode tree", e); @@ -32,20 +170,20 @@ public static JsonNode deserializeIntoTree(String contents, String fileOrHost) { } public static T deserialize(Object contents, String fileOrHost, Class expectedType) { - T result; + return deserialize(contents, fileOrHost, expectedType, null); + } - boolean isJson = false; + public static T deserialize(Object contents, String fileOrHost, Class expectedType, SwaggerDeserializationResult errorOutput) { + T result; - if(contents instanceof String && isJson((String)contents)) { - isJson = true; - } + boolean isJson = contents instanceof String && isJson((String) contents); try { if (contents instanceof String) { if (isJson) { result = Json.mapper().readValue((String) contents, expectedType); } else { - result = Yaml.mapper().readValue((String) contents, expectedType); + result = getYamlMapper().convertValue(readYamlTree((String) contents, errorOutput), expectedType); } } else { result = Json.mapper().convertValue(contents, expectedType); @@ -58,16 +196,244 @@ public static T deserialize(Object contents, String fileOrHost, Class exp } private static boolean isJson(String contents) { - return contents.toString().trim().startsWith("{"); + return contents.trim().startsWith("{"); + } + + + public static org.yaml.snakeyaml.Yaml buildSnakeYaml(BaseConstructor constructor) { + try { + LoaderOptions.class.getMethod("getMaxAliasesForCollections"); + } catch (NoSuchMethodException e) { + return new org.yaml.snakeyaml.Yaml(constructor); + } + try { + LoaderOptions loaderOptions = buildLoaderOptions(); + org.yaml.snakeyaml.Yaml yaml = new org.yaml.snakeyaml.Yaml(constructor, new Representer(new DumperOptions()), new DumperOptions(), loaderOptions, new CustomResolver()); + return yaml; + } catch (Exception e) { + // + LOGGER.error("error building snakeYaml", e); + } + return new org.yaml.snakeyaml.Yaml(constructor); + } + + public static LoaderOptions buildLoaderOptions() { + LoaderOptions loaderOptions = new LoaderOptions(); + try { + Method method = LoaderOptions.class.getMethod("setMaxAliasesForCollections", int.class); + method.invoke(loaderOptions, options.getMaxYamlAliasesForCollections()); + method = LoaderOptions.class.getMethod("setAllowRecursiveKeys", boolean.class); + method.invoke(loaderOptions, options.isYamlAllowRecursiveKeys()); + method = LoaderOptions.class.getMethod("setCodePointLimit", int.class); + method.invoke(loaderOptions, options.getMaxYamlCodePoints()); + } catch (ReflectiveOperationException e) { + LOGGER.debug("using snakeyaml < 1.25, not setting YAML Billion Laughs Attack snakeyaml level protection"); + } + return loaderOptions; + } + + public static JsonNode readYamlTree(String contents, SwaggerDeserializationResult errorOutput) throws IOException { + + if (!options.isSupportYamlAnchors()) { + return getYamlMapper().readTree(contents); + } + try { + org.yaml.snakeyaml.Yaml yaml = null; + if (options.isValidateYamlInput()) { + yaml = buildSnakeYaml(new CustomSnakeYamlConstructor()); + } else { + yaml = buildSnakeYaml(new SafeConstructor(buildLoaderOptions())); + } + + Object o = yaml.load(contents); + if (options.isValidateYamlInput()) { + boolean res = exceedsLimits(o, null, new Integer(0), new IdentityHashMap(), errorOutput); + if (res) { + LOGGER.warn("Error converting snake-parsed yaml to JsonNode"); + return getYamlMapper().readTree(contents); + } + } + JsonNode n = Json.mapper().convertValue(o, JsonNode.class); + return n; + + + } catch (Throwable e) { + LOGGER.warn("Error snake-parsing yaml content", e); + if (errorOutput != null) { + errorOutput.message(e.getMessage()); + } + return getYamlMapper().readTree(contents); + } + } + + private static boolean exceedsLimits(Object o, Object parent, Integer depth, Map visited, SwaggerDeserializationResult errorOutput) { + + if (o == null) return false; + if (!(o instanceof List) && !(o instanceof Map)) return false; + if (depth > options.getMaxYamlDepth()) { + String msg = String.format("snake-yaml result exceeds max depth %d; threshold can be increased if needed by setting system property `maxYamlDepth` to a higher value.", options.getMaxYamlDepth()); + LOGGER.warn(msg); + if (errorOutput != null) { + errorOutput.message(msg); + } + return true; + } + int currentDepth = depth; + if (visited.containsKey(o)) { + Object target = parent; + if (target == null) { + target = o; + } + if (options.isYamlCycleCheck()) { + boolean res = hasReference(o, target, new Integer(0), new IdentityHashMap(), errorOutput); + if (res) { + return true; + } + } + if (visited.get(o) > options.getMaxYamlReferences()) { + String msg = String.format("snake-yaml result exceeds max references %d; threshold can be increased if needed by setting system property `maxYamlReferences` to a higher value.", options.getMaxYamlReferences()); + LOGGER.warn(msg); + if (errorOutput != null) { + errorOutput.message(msg); + } + return true; + } + visited.put(o, visited.get(o) + 1); + + } else { + visited.put(o, 1L); + } + + if (o instanceof Map) { + for (Object k : ((Map) o).keySet()) { + boolean res = exceedsLimits(k, o, currentDepth + 1, visited, errorOutput); + if (res) { + return true; + } + } + for (Object v : ((Map) o).values()) { + boolean res = exceedsLimits(v, o, currentDepth + 1, visited, errorOutput); + if (res) { + return true; + } + } + + } else if (o instanceof List) { + for (Object v: ((List)o)) { + boolean res = exceedsLimits(v, o, currentDepth + 1, visited, errorOutput); + if (res) { + return true; + } + } + } + return false; + } + + private static boolean hasReference(Object o, Object target, Integer depth, Map visited, SwaggerDeserializationResult errorOutput) { + + if (o == null || target == null) return false; + if (!(o instanceof List) && !(o instanceof Map)) return false; + if (!(target instanceof List) && !(target instanceof Map)) return false; + if (depth > options.getMaxYamlDepth()) { + String msg = String.format("snake-yaml result exceeds max depth %d; threshold can be increased if needed by setting system property `maxYamlDepth` to a higher value.", options.getMaxYamlDepth()); + LOGGER.warn(msg); + if (errorOutput != null) { + errorOutput.message(msg); + } + return true; + } + int currentDepth = depth; + if (visited.containsKey(target)) { + return false; + } + visited.put(o, 1L); + ArrayList children = new ArrayList(); + if (o instanceof Map) { + children.addAll(((Map)o).keySet()); + children.addAll(((Map)o).values()); + + } else if (o instanceof List) { + children.addAll((List)o); + } + for (Object v : children) { + if (v == target) { + String msg = "detected cycle in snake-yaml result; cycle check can be disabled by setting system property `yamlCycleCheck` to false."; + LOGGER.warn(msg); + if (errorOutput != null) { + errorOutput.message(msg); + } + return true; + } + boolean res = hasReference(v, target, currentDepth + 1, visited, errorOutput); + if (res) { + return true; + } + } + return false; } - public static JsonNode readYamlTree(String contents) { - org.yaml.snakeyaml.Yaml yaml = new org.yaml.snakeyaml.Yaml(new SafeConstructor()); - return Json.mapper().convertValue(yaml.load(contents), JsonNode.class); + static class SnakeException extends RuntimeException { + public SnakeException() { + super(); + } + public SnakeException(String msg) { + super(msg); + } + + public SnakeException(String message, Throwable cause) { + super(message, cause); + } + } - public static T readYamlValue(String contents, Class expectedType) { - org.yaml.snakeyaml.Yaml yaml = new org.yaml.snakeyaml.Yaml(new SafeConstructor()); - return Json.mapper().convertValue(yaml.load(contents), expectedType); + static class CustomSnakeYamlConstructor extends SafeConstructor { + + public CustomSnakeYamlConstructor() { + super(buildLoaderOptions()); + } + private boolean checkNode(MappingNode node, Integer depth) { + if (node.getValue() == null) return true; + if (depth > options.getMaxYamlDepth()) return false; + int currentDepth = depth; + List list = node.getValue(); + for (NodeTuple t : list) { + Node n = t.getKeyNode(); + if (n instanceof MappingNode) { + boolean res = checkNode((MappingNode) n, currentDepth + 1); + if (!res) { + return false; + } + } + } + return true; + } + + @Override + public Object getSingleData(Class type) { + try { + Node node = this.composer.getSingleNode(); + if (node != null) { + if (node instanceof MappingNode) { + if (!checkNode((MappingNode) node, new Integer(0))) { + LOGGER.warn("yaml tree depth exceeds max depth {}; threshold can be increased if needed by setting system property `maxYamlDepth` to a higher value.", options.getMaxYamlDepth()); + throw new SnakeException("yaml tree depth exceeds max " + options.getMaxYamlDepth()); + } + } + if (Object.class != type) { + node.setTag(new Tag(type)); + } else if (this.rootTag != null) { + node.setTag(this.rootTag); + } + + return this.constructDocument(node); + } else { + return null; + } + } catch (StackOverflowError e) { + throw new SnakeException("StackOverflow safe-checking yaml content (maxDepth " + options.getMaxYamlDepth() + ")", e); + } catch (Throwable e) { + throw new SnakeException("Exception safe-checking yaml content (maxDepth " + options.getMaxYamlDepth() + ", maxYamlAliasesForCollections " + options.getMaxYamlAliasesForCollections() + ")", e); + } + } } } diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/util/InlineModelResolver.java b/modules/swagger-parser/src/main/java/io/swagger/parser/util/InlineModelResolver.java new file mode 100644 index 0000000000..1574e3b0df --- /dev/null +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/util/InlineModelResolver.java @@ -0,0 +1,528 @@ +package io.swagger.parser.util; + +import io.swagger.models.*; +import io.swagger.models.parameters.BodyParameter; +import io.swagger.models.parameters.Parameter; +import io.swagger.models.properties.*; +import io.swagger.models.utils.PropertyModelConverter; +import io.swagger.util.Json; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class InlineModelResolver { + private Swagger swagger; + private boolean skipMatches; + static Logger LOGGER = LoggerFactory.getLogger(InlineModelResolver.class); + + Map addedModels = new HashMap(); + Map generatedSignature = new HashMap(); + + public void flatten(Swagger swagger) { + this.swagger = swagger; + + if (swagger.getDefinitions() == null) { + swagger.setDefinitions(new HashMap()); + } + + // operations + Map paths = swagger.getPaths(); + Map models = swagger.getDefinitions(); + + if (paths != null) { + for (String pathname : paths.keySet()) { + Path path = paths.get(pathname); + + for (Operation operation : path.getOperations()) { + List parameters = operation.getParameters(); + + if (parameters != null) { + for (Parameter parameter : parameters) { + if (parameter instanceof BodyParameter) { + BodyParameter bp = (BodyParameter) parameter; + if (bp.getSchema() != null) { + Model model = bp.getSchema(); + if (model instanceof ModelImpl) { + ModelImpl obj = (ModelImpl) model; + if (obj.getType() == null || "object".equals(obj.getType())) { + if (obj.getProperties() != null && obj.getProperties().size() > 0) { + flattenProperties(obj.getProperties(), pathname); + String modelName = resolveModelName(obj.getTitle(), bp.getName()); + bp.setSchema(new RefModel(modelName)); + addGenerated(modelName, model); + swagger.addDefinition(modelName, model); + } + } + } else if (model instanceof ArrayModel) { + ArrayModel am = (ArrayModel) model; + Property inner = am.getItems(); + + if (inner instanceof ObjectProperty) { + ObjectProperty op = (ObjectProperty) inner; + if (op.getProperties() != null && op.getProperties().size() > 0) { + flattenProperties(op.getProperties(), pathname); + String modelName = resolveModelName(op.getTitle(), bp.getName()); + Model innerModel = modelFromProperty(op, modelName); + String existing = matchGenerated(innerModel); + if (existing != null) { + RefProperty refProperty = new RefProperty(existing); + refProperty.setRequired(op.getRequired()); + am.setItems(refProperty); + } else { + RefProperty refProperty = new RefProperty(modelName); + refProperty.setRequired(op.getRequired()); + am.setItems(refProperty); + addGenerated(modelName, innerModel); + swagger.addDefinition(modelName, innerModel); + } + } + } + } + } + } + } + } + Map responses = operation.getResponses(); + if (responses != null) { + for (String key : responses.keySet()) { + Response response = responses.get(key); + if (response.getSchema() != null) { + Property property = response.getSchema(); + if (property instanceof ObjectProperty) { + ObjectProperty op = (ObjectProperty) property; + if (op.getProperties() != null && op.getProperties().size() > 0) { + String modelName = resolveModelName(op.getTitle(), "inline_response_" + key); + Model model = modelFromProperty(op, modelName); + String existing = matchGenerated(model); + if (existing != null) { + Property refProperty = this.makeRefProperty(existing, property); + refProperty.setRequired(op.getRequired()); + response.setResponseSchema(new PropertyModelConverter().propertyToModel(refProperty)); + } else { + Property refProperty = this.makeRefProperty(modelName, property); + refProperty.setRequired(op.getRequired()); + response.setResponseSchema(new PropertyModelConverter().propertyToModel(refProperty)); + addGenerated(modelName, model); + swagger.addDefinition(modelName, model); + } + } + } else if (property instanceof ArrayProperty) { + ArrayProperty ap = (ArrayProperty) property; + Property inner = ap.getItems(); + + if (inner instanceof ObjectProperty) { + ObjectProperty op = (ObjectProperty) inner; + if (op.getProperties() != null && op.getProperties().size() > 0) { + flattenProperties(op.getProperties(), pathname); + String modelName = resolveModelName(op.getTitle(), + "inline_response_" + key); + Model innerModel = modelFromProperty(op, modelName); + String existing = matchGenerated(innerModel); + if (existing != null) { + Property refProperty = this.makeRefProperty(existing, op); + refProperty.setRequired(op.getRequired()); + ap.setItems(refProperty); + response.setResponseSchema(new PropertyModelConverter().propertyToModel(ap)); + } else { + Property refProperty = this.makeRefProperty(modelName, op); + refProperty.setRequired(op.getRequired()); + ap.setItems(refProperty); + response.setResponseSchema(new PropertyModelConverter().propertyToModel(ap)); + addGenerated(modelName, innerModel); + swagger.addDefinition(modelName, innerModel); + } + } + } + } else if (property instanceof MapProperty) { + MapProperty mp = (MapProperty) property; + + Property innerProperty = mp.getAdditionalProperties(); + if (innerProperty instanceof ObjectProperty) { + ObjectProperty op = (ObjectProperty) innerProperty; + if (op.getProperties() != null && op.getProperties().size() > 0) { + flattenProperties(op.getProperties(), pathname); + String modelName = resolveModelName(op.getTitle(), + "inline_response_" + key); + Model innerModel = modelFromProperty(op, modelName); + String existing = matchGenerated(innerModel); + if (existing != null) { + RefProperty refProperty = new RefProperty(existing); + refProperty.setRequired(op.getRequired()); + mp.setAdditionalProperties(refProperty); + response.setResponseSchema(new PropertyModelConverter().propertyToModel(mp)); + } else { + RefProperty refProperty = new RefProperty(modelName); + refProperty.setRequired(op.getRequired()); + mp.setAdditionalProperties(refProperty); + response.setResponseSchema(new PropertyModelConverter().propertyToModel(mp)); + addGenerated(modelName, innerModel); + swagger.addDefinition(modelName, innerModel); + } + } + } + } + } + } + } + } + } + } + + // definitions + if (models != null) { + List modelNames = new ArrayList(models.keySet()); + for (String modelName : modelNames) { + Model model = models.get(modelName); + if (model instanceof ModelImpl) { + ModelImpl m = (ModelImpl) model; + + Map properties = m.getProperties(); + flattenProperties(properties, modelName); + fixStringModel(m); + } else if (model instanceof ArrayModel) { + ArrayModel m = (ArrayModel) model; + Property inner = m.getItems(); + if (inner instanceof ObjectProperty) { + ObjectProperty op = (ObjectProperty) inner; + if (op.getProperties() != null && op.getProperties().size() > 0) { + String innerModelName = resolveModelName(op.getTitle(), modelName + "_inner"); + Model innerModel = modelFromProperty(op, innerModelName); + String existing = matchGenerated(innerModel); + if (existing == null) { + swagger.addDefinition(innerModelName, innerModel); + addGenerated(innerModelName, innerModel); + RefProperty refProperty = new RefProperty(innerModelName); + refProperty.setRequired(op.getRequired()); + m.setItems(refProperty); + } else { + RefProperty refProperty = new RefProperty(existing); + refProperty.setRequired(op.getRequired()); + m.setItems(refProperty); + } + } + } + } else if (model instanceof ComposedModel) { + ComposedModel m = (ComposedModel) model; + if (m.getChild() != null) { + Map properties = m.getChild().getProperties(); + flattenProperties(properties, modelName); + } + } + } + } + } + + /** + * This function fix models that are string (mostly enum). Before this fix, the example + * would look something like that in the doc: "\"example from def\"" + * @param m Model implementation + */ + private void fixStringModel(ModelImpl m) { + if (m.getType() != null && m.getType().equals("string") && m.getExample() != null) { + String example = m.getExample().toString(); + if (!example.isEmpty() && example.substring(0, 1).equals("\"") && + example.substring(example.length() - 1).equals("\"")) { + m.setExample(example.substring(1, example.length() - 1)); + } + } + } + + private String resolveModelName(String title, String key) { + if (title == null) { + return uniqueName(key); + } else { + return uniqueName(title); + } + } + + public String matchGenerated(Model model) { + if (this.skipMatches) { + return null; + } + String json = Json.pretty(model); + if (generatedSignature.containsKey(json)) { + return generatedSignature.get(json); + } + return null; + } + + public void addGenerated(String name, Model model) { + generatedSignature.put(Json.pretty(model), name); + } + + public String uniqueName(String key) { + int count = 0; + boolean done = false; + key = key.replaceAll("[^a-z_\\.A-Z0-9 ]", ""); // FIXME: a parameter + // should not be + // assigned. Also declare + // the methods parameters + // as 'final'. + while (!done) { + String name = key; + if (count > 0) { + name = key + "_" + count; + } + if (swagger.getDefinitions() == null) { + return name; + } else if (!swagger.getDefinitions().containsKey(name)) { + return name; + } + count += 1; + } + return key; + } + + public void flattenProperties(Map properties, String path) { + if (properties == null) { + return; + } + Map propsToUpdate = new HashMap(); + Map modelsToAdd = new HashMap(); + for (String key : properties.keySet()) { + Property property = properties.get(key); + if (property instanceof ObjectProperty && ((ObjectProperty) property).getProperties() != null + && ((ObjectProperty) property).getProperties().size() > 0) { + + ObjectProperty op = (ObjectProperty) property; + + String modelName = resolveModelName(op.getTitle(), path + "_" + key); + Model model = modelFromProperty(op, modelName); + + String existing = matchGenerated(model); + + if (existing != null) { + RefProperty refProperty = new RefProperty(existing); + refProperty.setRequired(op.getRequired()); + propsToUpdate.put(key, refProperty); + } else { + RefProperty refProperty = new RefProperty(modelName); + refProperty.setRequired(op.getRequired()); + propsToUpdate.put(key, refProperty); + modelsToAdd.put(modelName, model); + addGenerated(modelName, model); + swagger.addDefinition(modelName, model); + } + } else if (property instanceof ArrayProperty) { + ArrayProperty ap = (ArrayProperty) property; + Property inner = ap.getItems(); + + if (inner instanceof ObjectProperty) { + ObjectProperty op = (ObjectProperty) inner; + if (op.getProperties() != null && op.getProperties().size() > 0) { + flattenProperties(op.getProperties(), path); + String modelName = resolveModelName(op.getTitle(), path + "_" + key); + Model innerModel = modelFromProperty(op, modelName); + String existing = matchGenerated(innerModel); + if (existing != null) { + RefProperty refProperty = new RefProperty(existing); + refProperty.setRequired(op.getRequired()); + ap.setItems(refProperty); + } else { + RefProperty refProperty = new RefProperty(modelName); + refProperty.setRequired(op.getRequired()); + ap.setItems(refProperty); + addGenerated(modelName, innerModel); + swagger.addDefinition(modelName, innerModel); + } + } + } + } else if (property instanceof MapProperty) { + MapProperty mp = (MapProperty) property; + Property inner = mp.getAdditionalProperties(); + + if (inner instanceof ObjectProperty) { + ObjectProperty op = (ObjectProperty) inner; + if (op.getProperties() != null && op.getProperties().size() > 0) { + flattenProperties(op.getProperties(), path); + String modelName = resolveModelName(op.getTitle(), path + "_" + key); + Model innerModel = modelFromProperty(op, modelName); + String existing = matchGenerated(innerModel); + if (existing != null) { + RefProperty refProperty = new RefProperty(existing); + refProperty.setRequired(op.getRequired()); + mp.setAdditionalProperties(refProperty); + } else { + RefProperty refProperty = new RefProperty(modelName); + refProperty.setRequired(op.getRequired()); + mp.setAdditionalProperties(refProperty); + addGenerated(modelName, innerModel); + swagger.addDefinition(modelName, innerModel); + } + } + } + } else if (property instanceof ComposedProperty) { + ComposedProperty composedProperty = (ComposedProperty) property; + String modelName = resolveModelName(composedProperty.getTitle(), path + "_" + key); + Model model = modelFromProperty(composedProperty, modelName); + String existing = matchGenerated(model); + if (existing != null) { + RefProperty refProperty = new RefProperty(existing); + refProperty.setRequired(composedProperty.getRequired()); + propsToUpdate.put(key, refProperty); + } else { + RefProperty refProperty = new RefProperty(modelName); + refProperty.setRequired(composedProperty.getRequired()); + propsToUpdate.put(key, refProperty); + addGenerated(modelName, model); + swagger.addDefinition(modelName, model); + } + } + } + if (propsToUpdate.size() > 0) { + for (String key : propsToUpdate.keySet()) { + properties.put(key, propsToUpdate.get(key)); + } + } + for (String key : modelsToAdd.keySet()) { + swagger.addDefinition(key, modelsToAdd.get(key)); + this.addedModels.put(key, modelsToAdd.get(key)); + } + } + + @SuppressWarnings("static-method") + public Model modelFromProperty(ArrayProperty object, @SuppressWarnings("unused") String path) { + String description = object.getDescription(); + String example = null; + + Object obj = object.getExample(); + if (obj != null) { + example = obj.toString(); + } + + Property inner = object.getItems(); + if (inner instanceof ObjectProperty) { + ArrayModel model = new ArrayModel(); + model.setDescription(description); + model.setExample(example); + model.setItems(object.getItems()); + if (object.getVendorExtensions() != null) { + for (String key : object.getVendorExtensions().keySet()) { + model.setVendorExtension(key, object.getVendorExtensions().get(key)); + } + } + + return model; + } + + return null; + } + + public Model modelFromProperty(ObjectProperty object, String path) { + String description = object.getDescription(); + String example = null; + + Object obj = object.getExample(); + if (obj != null) { + example = obj.toString(); + } + String name = object.getName(); + Xml xml = object.getXml(); + Map properties = object.getProperties(); + + ModelImpl model = new ModelImpl(); + model.type(object.getType()); + model.setDescription(description); + model.setExample(example); + model.setName(name); + model.setXml(xml); + if (object.getVendorExtensions() != null) { + for (String key : object.getVendorExtensions().keySet()) { + model.setVendorExtension(key, object.getVendorExtensions().get(key)); + } + } + + if (properties != null) { + flattenProperties(properties, path); + model.setProperties(properties); + } + + return model; + } + + public Model modelFromProperty(ComposedProperty composedProperty, String path) { + String description = composedProperty.getDescription(); + String example = null; + + Object obj = composedProperty.getExample(); + if (obj != null) { + example = obj.toString(); + } + Xml xml = composedProperty.getXml(); + + ModelImpl model = new ModelImpl(); + model.type(composedProperty.getType()); + model.setDescription(description); + model.setExample(example); + model.setName(path); + model.setXml(xml); + if (composedProperty.getVendorExtensions() != null) { + for (String key : composedProperty.getVendorExtensions().keySet()) { + model.setVendorExtension(key, composedProperty.getVendorExtensions().get(key)); + } + } + return model; + } + + @SuppressWarnings("static-method") + public Model modelFromProperty(MapProperty object, @SuppressWarnings("unused") String path) { + String description = object.getDescription(); + String example = null; + + Object obj = object.getExample(); + if (obj != null) { + example = obj.toString(); + } + + ArrayModel model = new ArrayModel(); + model.setDescription(description); + model.setExample(example); + model.setItems(object.getAdditionalProperties()); + if (object.getVendorExtensions() != null) { + for (String key : object.getVendorExtensions().keySet()) { + model.setVendorExtension(key, object.getVendorExtensions().get(key)); + } + } + + return model; + } + + /** + * Make a RefProperty + * + * @param ref new property name + * @param property Property + * @return {@link Property} A constructed Swagger property + */ + public Property makeRefProperty(String ref, Property property) { + RefProperty newProperty = new RefProperty(ref); + this.copyVendorExtensions(property, newProperty); + return newProperty; + } + + /** + * Copy vendor extensions from Property to another Property + * + * @param source source property + * @param target target property + */ + public void copyVendorExtensions(Property source, AbstractProperty target) { + Map vendorExtensions = source.getVendorExtensions(); + for (String extName : vendorExtensions.keySet()) { + target.setVendorExtension(extName, vendorExtensions.get(extName)); + } + } + + public boolean isSkipMatches() { + return skipMatches; + } + + public void setSkipMatches(boolean skipMatches) { + this.skipMatches = skipMatches; + } + +} diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/util/ParseOptions.java b/modules/swagger-parser/src/main/java/io/swagger/parser/util/ParseOptions.java new file mode 100644 index 0000000000..64e9946a9d --- /dev/null +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/util/ParseOptions.java @@ -0,0 +1,47 @@ +package io.swagger.parser.util; + +import java.util.List; + +public class ParseOptions { + private boolean resolve; + private boolean flatten; + private boolean safelyResolveURL; + private List remoteRefAllowList; + private List remoteRefBlockList; + + public boolean isResolve() { + return resolve; + } + + public void setResolve(boolean resolve) { + this.resolve = resolve; + } + + public boolean isFlatten() { return flatten; } + + public void setFlatten(boolean flatten) { this.flatten = flatten; } + + public boolean isSafelyResolveURL() { + return safelyResolveURL; + } + + public void setSafelyResolveURL(boolean safelyResolveURL) { + this.safelyResolveURL = safelyResolveURL; + } + + public List getRemoteRefAllowList() { + return remoteRefAllowList; + } + + public void setRemoteRefAllowList(List remoteRefAllowList) { + this.remoteRefAllowList = remoteRefAllowList; + } + + public List getRemoteRefBlockList() { + return remoteRefBlockList; + } + + public void setRemoteRefBlockList(List remoteRefBlockList) { + this.remoteRefBlockList = remoteRefBlockList; + } +} diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/util/RefUtils.java b/modules/swagger-parser/src/main/java/io/swagger/parser/util/RefUtils.java index 42f9dd0128..ed53f3822d 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/util/RefUtils.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/util/RefUtils.java @@ -2,11 +2,14 @@ import io.swagger.models.auth.AuthorizationValue; import io.swagger.models.refs.RefFormat; +import io.swagger.parser.processors.ExternalRefProcessor; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import java.io.File; import java.io.FileInputStream; +import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystem; @@ -78,45 +81,7 @@ public static String readExternalUrlRef(String file, RefFormat refFormat, List auths) throws Exception { @@ -99,10 +101,12 @@ public static String urlToString(String url, List auths) thr final List header = new ArrayList<>(); if (auths != null) { for (AuthorizationValue auth : auths) { - if ("query".equals(auth.getType())) { - appendValue(inUrl, auth, query); - } else if ("header".equals(auth.getType())) { - appendValue(inUrl, auth, header); + if (auth.getUrlMatcher() == null || auth.getUrlMatcher().test(inUrl)) { + if ("query".equals(auth.getType())) { + appendValue(inUrl, auth, query); + } else if ("header".equals(auth.getType())) { + appendValue(inUrl, auth, header); + } } } } @@ -131,7 +135,8 @@ public static String urlToString(String url, List auths) thr conn.connect(); url = ((HttpURLConnection) conn).getHeaderField("Location"); - } while (301 == ((HttpURLConnection) conn).getResponseCode()); + } while ((301 == ((HttpURLConnection) conn).getResponseCode())||(302 == ((HttpURLConnection) conn).getResponseCode()) + || (307 == ((HttpURLConnection) conn).getResponseCode())||(308 == ((HttpURLConnection) conn).getResponseCode())); InputStream in = conn.getInputStream(); diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/util/SwaggerDeserializer.java b/modules/swagger-parser/src/main/java/io/swagger/parser/util/SwaggerDeserializer.java index 8c36b3dc06..166b7d2b30 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/util/SwaggerDeserializer.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/util/SwaggerDeserializer.java @@ -5,13 +5,16 @@ import io.swagger.models.*; import io.swagger.models.auth.*; import io.swagger.models.parameters.*; +import io.swagger.models.properties.ArrayProperty; +import io.swagger.models.properties.BooleanValueProperty; import io.swagger.models.properties.Property; import io.swagger.models.properties.PropertyBuilder; -import io.swagger.models.properties.RefProperty; import io.swagger.util.Json; import java.math.BigDecimal; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static io.swagger.models.properties.PropertyBuilder.PropertyId.*; @@ -30,7 +33,9 @@ public class SwaggerDeserializer { protected static Set OPERATION_KEYS = new LinkedHashSet(Arrays.asList("scheme", "tags", "summary", "description", "externalDocs", "operationId", "consumes", "produces", "parameters", "responses", "schemes", "deprecated", "security")); protected static Set PARAMETER_KEYS = new LinkedHashSet(Arrays.asList("name", "in", "description", "required", "type", "format", "allowEmptyValue", "items", "collectionFormat", "default", "maximum", "exclusiveMaximum", "minimum", "exclusiveMinimum", "maxLength", "minLength", "pattern", "maxItems", "minItems", "uniqueItems", "enum", "multipleOf", "readOnly", "allowEmptyValue")); protected static Set BODY_PARAMETER_KEYS = new LinkedHashSet(Arrays.asList("name", "in", "description", "required", "schema")); - protected static Set SECURITY_SCHEME_KEYS = new LinkedHashSet(Arrays.asList("type", "name", "in", "description", "flow", "authorizationUrl", "tokenUrl" , "scopes")); + protected static Set SECURITY_SCHEME_KEYS = new LinkedHashSet(Arrays.asList("type", "name", "in", "description", "flow", "authorizationUrl", "tokenUrl", "scopes")); + + private final Set operationIDs = new HashSet<>(); public SwaggerDeserializationResult deserialize(JsonNode rootNode) { SwaggerDeserializationResult result = new SwaggerDeserializationResult(); @@ -46,7 +51,7 @@ public Swagger parseRoot(JsonNode node, ParseResult result) { String location = ""; Swagger swagger = new Swagger(); if (node.getNodeType().equals(JsonNodeType.OBJECT)) { - ObjectNode on = (ObjectNode)node; + ObjectNode on = (ObjectNode) node; Iterator it = null; // required @@ -54,7 +59,7 @@ public Swagger parseRoot(JsonNode node, ParseResult result) { swagger.setSwagger(value); ObjectNode obj = getObject("info", on, true, "", result); - if(obj != null) { + if (obj != null) { Info info = info(obj, "info", result); swagger.info(info); } @@ -67,7 +72,7 @@ public Swagger parseRoot(JsonNode node, ParseResult result) { swagger.setBasePath(value); ArrayNode array = getArray("schemes", on, false, location, result); - if(array != null) { + if (array != null) { it = array.iterator(); while (it.hasNext()) { JsonNode n = it.next(); @@ -82,7 +87,7 @@ public Swagger parseRoot(JsonNode node, ParseResult result) { } array = getArray("consumes", on, false, location, result); - if(array != null) { + if (array != null) { it = array.iterator(); while (it.hasNext()) { JsonNode n = it.next(); @@ -94,7 +99,7 @@ public Swagger parseRoot(JsonNode node, ParseResult result) { } array = getArray("produces", on, false, location, result); - if(array != null) { + if (array != null) { it = array.iterator(); while (it.hasNext()) { JsonNode n = it.next(); @@ -116,13 +121,16 @@ public Swagger parseRoot(JsonNode node, ParseResult result) { obj = getObject("parameters", on, false, location, result); // TODO: parse - if(obj != null) { + if (obj != null) { Map parameters = new LinkedHashMap<>(); Set keys = getKeys(obj); - for(String key : keys) { + for (String key : keys) { JsonNode paramNode = obj.get(key); - if(paramNode instanceof ObjectNode) { - Parameter parameter = this.parameter((ObjectNode)paramNode, location, result); + if (paramNode instanceof ObjectNode) { + Parameter parameter = this.parameter((ObjectNode) paramNode, location, result); + if ("path".equalsIgnoreCase(parameter.getIn()) && !parameter.getRequired()) { + result.warning(location + ".'" + parameter.getName() + "'", " For path parameter '" + parameter.getName() + "' the required value should be true"); + } parameters.put(key, parameter); } } @@ -130,7 +138,7 @@ public Swagger parseRoot(JsonNode node, ParseResult result) { } obj = getObject("responses", on, false, location, result); - Map responses = responses(obj, "responses", result); + Responses responses = responses(obj, "responses", result); swagger.responses(responses); obj = getObject("securityDefinitions", on, false, location, result); @@ -151,16 +159,14 @@ public Swagger parseRoot(JsonNode node, ParseResult result) { // extra keys Set keys = getKeys(on); - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { swagger.vendorExtension(key, extension(on.get(key))); - } - else if(!ROOT_KEYS.contains(key)) { + } else if (!ROOT_KEYS.contains(key)) { result.extra(location, key, node.get(key)); } } - } - else { + } else { result.invalidType("", "", "object", node); result.invalid(); return null; @@ -168,24 +174,50 @@ else if(!ROOT_KEYS.contains(key)) { return swagger; } + public Map paths(ObjectNode obj, String location, ParseResult result) { - Map output = new LinkedHashMap<>(); + Map output = new LinkedHashMap<>(); if(obj == null) { return null; } Set pathKeys = getKeys(obj); - for(String pathName : pathKeys) { + for (String pathName : pathKeys) { JsonNode pathValue = obj.get(pathName); - if(pathName.startsWith("x-")) { + if (pathName.startsWith("x-")) { result.unsupported(location, pathName, pathValue); - } - else { + } else { if (!pathValue.getNodeType().equals(JsonNodeType.OBJECT)) { result.invalidType(location, pathName, "object", pathValue); } else { ObjectNode path = (ObjectNode) pathValue; Path pathObj = path(path, location + ".'" + pathName + "'", result); + List eachPart = new ArrayList<>(); + Matcher m = Pattern.compile("\\{(.+?)\\}").matcher(pathName); + while (m.find()) { + eachPart.add(m.group()); + } + List operationsInAPath = getAllOperationsInAPath(pathObj); + for (Operation operation : operationsInAPath) { + for (Parameter parameter : operation.getParameters()) { + if ("path".equalsIgnoreCase(parameter.getIn()) && !parameter.getRequired()) { + result.warning(location + ".'" + parameter.getName() + "'", " For path parameter '" + parameter.getName() + "' the required value should be true"); + } + } + } + for (String part : eachPart) { + String pathParam = part.substring(1, part.length() - 1); + boolean definedInPathLevel = isPathParamDefined(pathParam, pathObj.getParameters()); + if (definedInPathLevel) { + continue; + } + for (Operation operation : operationsInAPath) { + if (!isPathParamDefined(pathParam, operation.getParameters())) { + result.warning(location + ".'" + pathName + "'", " Declared path parameter " + pathParam + " needs to be defined as a path parameter in path or operation level"); + break; + } + } + } output.put(pathName, pathObj); } } @@ -193,22 +225,53 @@ public Map paths(ObjectNode obj, String location, ParseResult resul return output; } - public Path path(ObjectNode obj, String location, ParseResult result) { - boolean hasRef = false; - Path output = null; - if(obj.get("$ref") != null) { - JsonNode ref = obj.get("$ref"); - if(ref.getNodeType().equals(JsonNodeType.STRING)) { + private boolean isPathParamDefined(String pathParam, List parameters) { + if (parameters == null || parameters.isEmpty()) { + return false; + } else { + for (Parameter parameter : parameters) { + if (parameter instanceof RefParameter || (pathParam.equals(parameter.getName()) && "path".equals(parameter.getIn()))) { + if (!parameter.getRequired()) { - return pathRef((TextNode)ref, location, result); + } + return true; + } } + } + return false; + } + + private void addToOperationsList(List operationsList, Operation operation) { + if (operation == null) { + return; + } + operationsList.add(operation); + } - else if(ref.getNodeType().equals(JsonNodeType.OBJECT)){ + private List getAllOperationsInAPath(Path pathObj) { + List operations = new ArrayList<>(); + addToOperationsList(operations, pathObj.getGet()); + addToOperationsList(operations, pathObj.getPut()); + addToOperationsList(operations, pathObj.getPost()); + addToOperationsList(operations, pathObj.getPatch()); + addToOperationsList(operations, pathObj.getDelete()); + addToOperationsList(operations, pathObj.getOptions()); + addToOperationsList(operations, pathObj.getHead()); + return operations; + } + + public Path path(ObjectNode obj, String location, ParseResult result) { + if (obj.get("$ref") != null) { + JsonNode ref = obj.get("$ref"); + if (ref.getNodeType().equals(JsonNodeType.STRING)) { + + return pathRef((TextNode) ref, location, result); + } else if (ref.getNodeType().equals(JsonNodeType.OBJECT)) { ObjectNode on = (ObjectNode) ref; // extra keys Set keys = getKeys(on); - for(String key : keys) { + for (String key : keys) { result.extra(location, key, on.get(key)); } } @@ -220,62 +283,61 @@ else if(ref.getNodeType().equals(JsonNodeType.OBJECT)){ path.setParameters(parameters(parameters, location, result)); ObjectNode on = getObject("get", obj, false, location, result); - if(on != null) { + if (on != null) { Operation op = operation(on, location + "(get)", result); - if(op != null) { + if (op != null) { path.setGet(op); } } on = getObject("put", obj, false, location, result); - if(on != null) { + if (on != null) { Operation op = operation(on, location + "(put)", result); - if(op != null) { + if (op != null) { path.setPut(op); } } on = getObject("post", obj, false, location, result); - if(on != null) { + if (on != null) { Operation op = operation(on, location + "(post)", result); - if(op != null) { + if (op != null) { path.setPost(op); } } on = getObject("head", obj, false, location, result); - if(on != null) { + if (on != null) { Operation op = operation(on, location + "(head)", result); - if(op != null) { + if (op != null) { path.setHead(op); } } on = getObject("delete", obj, false, location, result); - if(on != null) { + if (on != null) { Operation op = operation(on, location + "(delete)", result); - if(op != null) { + if (op != null) { path.setDelete(op); } } on = getObject("patch", obj, false, location, result); - if(on != null) { + if (on != null) { Operation op = operation(on, location + "(patch)", result); - if(op != null) { + if (op != null) { path.setPatch(op); } } on = getObject("options", obj, false, location, result); - if(on != null) { + if (on != null) { Operation op = operation(on, location + "(options)", result); - if(op != null) { + if (op != null) { path.setOptions(op); } } // extra keys Set keys = getKeys(obj); - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { path.setVendorExtension(key, extension(obj.get(key))); - } - else if(!PATH_KEYS.contains(key)) { + } else if (!PATH_KEYS.contains(key)) { result.extra(location, key, obj.get(key)); } } @@ -283,13 +345,13 @@ else if(!PATH_KEYS.contains(key)) { } public Operation operation(ObjectNode obj, String location, ParseResult result) { - if(obj == null) { + if (obj == null) { return null; } Operation output = new Operation(); ArrayNode array = getArray("tags", obj, false, location, result); List tags = tagStrings(array, location, result); - if(tags != null) { + if (tags != null) { output.tags(tags); } String value = getString("summary", obj, false, location, result); @@ -302,54 +364,56 @@ public Operation operation(ObjectNode obj, String location, ParseResult result) ExternalDocs docs = externalDocs(externalDocs, location, result); output.setExternalDocs(docs); - value = getString("operationId", obj, false, location, result); + value = getString("operationId", obj, false, location, result, operationIDs); output.operationId(value); array = getArray("consumes", obj, false, location, result); - if(array != null) { - if (array.size() == 0) { - output.consumes(Collections. emptyList()); - } else { - Iterator it = array.iterator(); - while (it.hasNext()) { - JsonNode n = it.next(); - String s = getString(n, location + ".consumes", result); - if (s != null) { - output.consumes(s); - } - } - } + if (array != null) { + if (array.size() == 0) { + output.consumes(Collections.emptyList()); + } else { + Iterator it = array.iterator(); + while (it.hasNext()) { + JsonNode n = it.next(); + String s = getString(n, location + ".consumes", result); + if (s != null) { + output.consumes(s); + } + } + } } array = getArray("produces", obj, false, location, result); if (array != null) { - if (array.size() == 0) { - output.produces(Collections. emptyList()); - } else { - Iterator it = array.iterator(); - while (it.hasNext()) { - JsonNode n = it.next(); - String s = getString(n, location + ".produces", result); - if (s != null) { - output.produces(s); - } - } - } + if (array.size() == 0) { + output.produces(Collections.emptyList()); + } else { + Iterator it = array.iterator(); + while (it.hasNext()) { + JsonNode n = it.next(); + String s = getString(n, location + ".produces", result); + if (s != null) { + output.produces(s); + } + } + } } ArrayNode parameters = getArray("parameters", obj, false, location, result); output.setParameters(parameters(parameters, location, result)); - ObjectNode responses = getObject("responses", obj, true, location, result); - Map responsesObject = responses(responses, location, result); + ObjectNode responses = getObject("responses", obj, true, location+".responses", result); + Responses responsesObject = responses(responses, location+".responses", result); if(responsesObject != null && responsesObject.size() == 0) { + result.missing(location, "responses"); } - output.setResponses(responsesObject); + output.setResponsesObject(responsesObject); array = getArray("schemes", obj, false, location, result); - if(array != null) { + if (array != null) { Iterator it = array.iterator(); while (it.hasNext()) { JsonNode n = it.next(); + String s = getString(n, location + ".schemes", result); if (s != null) { Scheme scheme = Scheme.forValue(s); @@ -360,16 +424,20 @@ public Operation operation(ObjectNode obj, String location, ParseResult result) } } Boolean deprecated = getBoolean("deprecated", obj, false, location, result); - if(deprecated != null) { + if (deprecated != null) { output.setDeprecated(deprecated); } array = getArray("security", obj, false, location, result); List security = securityRequirements(array, location, result); - if(security != null) { + if (security != null) { List>> ss = new ArrayList<>(); for(SecurityRequirement s : security) { - if(s.getRequirements() != null && s.getRequirements().size() > 0) { - ss.add(s.getRequirements()); + if(s.getRequirements() != null) { + if (s.getRequirements().size() > 0) { + ss.add(s.getRequirements()); + } else { + ss.add(Collections.>emptyMap()); + } } } output.setSecurity(ss); @@ -377,11 +445,10 @@ public Operation operation(ObjectNode obj, String location, ParseResult result) // extra keys Set keys = getKeys(obj); - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { output.setVendorExtension(key, extension(obj.get(key))); - } - else if(!OPERATION_KEYS.contains(key)) { + } else if (!OPERATION_KEYS.contains(key)) { result.extra(location, key, obj.get(key)); } } @@ -397,12 +464,10 @@ public Boolean getBoolean(String key, ObjectNode node, boolean required, String result.missing(location, key); result.invalid(); } - } - else { - if(v.getNodeType().equals(JsonNodeType.BOOLEAN)) { + } else { + if (v.getNodeType().equals(JsonNodeType.BOOLEAN)) { value = v.asBoolean(); - } - else if(v.getNodeType().equals(JsonNodeType.STRING)) { + } else if (v.getNodeType().equals(JsonNodeType.STRING)) { String stringValue = v.textValue(); return Boolean.parseBoolean(stringValue); } @@ -412,13 +477,13 @@ else if(v.getNodeType().equals(JsonNodeType.STRING)) { public List parameters(ArrayNode obj, String location, ParseResult result) { List output = new ArrayList(); - if(obj == null) { + if (obj == null) { return output; } - for(JsonNode item : obj) { - if(item.getNodeType().equals(JsonNodeType.OBJECT)) { + for (JsonNode item : obj) { + if (item.getNodeType().equals(JsonNodeType.OBJECT)) { Parameter param = parameter((ObjectNode) item, location, result); - if(param != null) { + if (param != null) { output.add(param); } } @@ -428,17 +493,16 @@ public List parameters(ArrayNode obj, String location, ParseResult re public Parameter parameter(ObjectNode obj, String location, ParseResult result) { - if(obj == null) { + if (obj == null) { return null; } Parameter output = null; JsonNode ref = obj.get("$ref"); - if(ref != null) { - if(ref.getNodeType().equals(JsonNodeType.STRING)) { + if (ref != null) { + if (ref.getNodeType().equals(JsonNodeType.STRING)) { return refParameter((TextNode) ref, location, result); - } - else { + } else { result.invalidType(location, "$ref", "string", obj); return null; } @@ -446,64 +510,60 @@ public Parameter parameter(ObjectNode obj, String location, ParseResult result) String l = null; JsonNode ln = obj.get("name"); - if(ln != null) { + if (ln != null) { l = ln.asText(); - } - else { + } else { l = "['unknown']"; } location += ".[" + l + "]"; String value = getString("in", obj, true, location, result); - if(value != null) { + if (value != null) { String type = getString("type", obj, false, location, result); String format = getString("format", obj, false, location, result); AbstractSerializableParameter sp = null; - if("query".equals(value)) { + if ("query".equals(value)) { sp = new QueryParameter(); - } - else if ("header".equals(value)) { + } else if ("header".equals(value)) { sp = new HeaderParameter(); - } - else if ("path".equals(value)) { + } else if ("path".equals(value)) { sp = new PathParameter(); - } - else if ("formData".equals(value)) { + } else if ("formData".equals(value)) { sp = new FormParameter(); } - if(sp != null) { + if (sp != null) { // type is mandatory when sp != null - getString("type", obj, true, location, result); + String paramType = getString("type", obj, true, location, result); Map map = new LinkedHashMap(); map.put(TYPE, type); map.put(FORMAT, format); - String defaultValue = getString("default", obj, false, location, result); + String defaultValue = parameterDefault(obj, paramType, location, result); map.put(DEFAULT, defaultValue); sp.setDefault(defaultValue); BigDecimal bd = getBigDecimal("maximum", obj, false, location, result); - if(bd != null) { + if (bd != null) { map.put(MAXIMUM, bd); sp.setMaximum(bd); } Boolean bl = getBoolean("exclusiveMaximum", obj, false, location, result); - if(bl != null) { + if (bl != null) { map.put(EXCLUSIVE_MAXIMUM, bl); sp.setExclusiveMaximum(bl); } bd = getBigDecimal("minimum", obj, false, location, result); - if(bd != null) { + if (bd != null) { map.put(MINIMUM, bd); sp.setMinimum(bd); } bl = getBoolean("exclusiveMinimum", obj, false, location, result); - if(bl != null) { + if (bl != null) { map.put(EXCLUSIVE_MINIMUM, bl); sp.setExclusiveMinimum(bl); } @@ -537,7 +597,7 @@ else if ("formData".equals(value)) { sp.setMaxLength(iv); bd = getBigDecimal("multipleOf", obj, false, location, result); - if(bd != null) { + if (bd != null) { map.put(MULTIPLE_OF, bd); sp.setMultipleOf(bd.doubleValue()); } @@ -547,13 +607,12 @@ else if ("formData".equals(value)) { sp.setUniqueItems(uniqueItems); ArrayNode an = getArray("enum", obj, false, location, result); - if(an != null) { + if (an != null) { List _enum = new ArrayList(); - for(JsonNode n : an) { - if(n.isValueNode()) { + for (JsonNode n : an) { + if (n.isValueNode()) { _enum.add(n.asText()); - } - else { + } else { result.invalidType(location, "enum", "value", n); } } @@ -562,34 +621,33 @@ else if ("formData".equals(value)) { } bl = getBoolean("readOnly", obj, false, location, result); - if(bl != null) { + if (bl != null) { map.put(READ_ONLY, bl); sp.setReadOnly(bl); } bl = getBoolean("allowEmptyValue", obj, false, location, result); - if(bl != null) { + if (bl != null) { map.put(ALLOW_EMPTY_VALUE, bl); sp.setAllowEmptyValue(bl); } Property prop = PropertyBuilder.build(type, format, map); - if(prop != null) { + if (prop != null) { sp.setProperty(prop); ObjectNode items = getObject("items", obj, false, location, result); - if(items != null) { + if (items != null) { Property inner = schema(null, items, location, result); sp.setItems(inner); } } Set keys = getKeys(obj); - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { sp.setVendorExtension(key, extension(obj.get(key))); - } - else if(!PARAMETER_KEYS.contains(key)) { + } else if (!PARAMETER_KEYS.contains(key)) { result.extra(location, key, obj.get(key)); } } @@ -598,41 +656,39 @@ else if(!PARAMETER_KEYS.contains(key)) { sp.setCollectionFormat(collectionFormat); output = sp; - } - else if ("body".equals(value)) { + } else if ("body".equals(value)) { BodyParameter bp = new BodyParameter(); JsonNode node = obj.get("schema"); - if(node != null && node instanceof ObjectNode) { - bp.setSchema(this.definition((ObjectNode)node, location, result)); + if (node != null && node instanceof ObjectNode) { + bp.setSchema(this.definition((ObjectNode) node, location, result)); } // examples ObjectNode examplesNode = getObject("examples", obj, false, location, result); - if(examplesNode != null) { + if (examplesNode != null) { Map examples = Json.mapper().convertValue(examplesNode, Json.mapper().getTypeFactory().constructMapType(Map.class, String.class, Object.class)); bp.setExamples(examples); } // pattern String pat = getString("pattern", obj, false, location, result); - if(pat != null) { + if (pat != null) { bp.setPattern(pat); } // readOnly Boolean bl = getBoolean("readOnly", obj, false, location, result); - if(bl != null) { + if (bl != null) { bp.setReadOnly(bl); } // vendor extensions Set keys = getKeys(obj); - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { bp.setVendorExtension(key, extension(obj.get(key))); - } - else if(!BODY_PARAMETER_KEYS.contains(key)) { + } else if (!BODY_PARAMETER_KEYS.contains(key)) { result.extra(location, key, obj.get(key)); } } @@ -640,7 +696,7 @@ else if(!BODY_PARAMETER_KEYS.contains(key)) { // output = Json.mapper().convertValue(obj, Parameter.class); } - if(output != null) { + if (output != null) { value = getString("name", obj, true, location, result); output.setName(value); @@ -648,7 +704,7 @@ else if(!BODY_PARAMETER_KEYS.contains(key)) { output.setDescription(value); Boolean required = getBoolean("required", obj, false, location, result); - if(required != null) { + if (required != null) { output.setRequired(required); } } @@ -657,6 +713,15 @@ else if(!BODY_PARAMETER_KEYS.contains(key)) { return output; } + private String parameterDefault(ObjectNode node, String type, String location, ParseResult result) { + String key = "default"; + if (type != null && type.equals("array")) { + ArrayNode array = getArray(key, node, false, location, result); + return array != null ? array.toString() : null; + } + return getString(key, node, false, location, result); + } + private Property schema(Map schemaItems, JsonNode obj, String location, ParseResult result) { return Json.mapper().convertValue(obj, Property.class); } @@ -664,7 +729,7 @@ private Property schema(Map schemaItems, JsonNode obj, String lo public RefParameter refParameter(TextNode obj, String location, ParseResult result) { return new RefParameter(obj.asText()); } - + public RefResponse refResponse(TextNode obj, String location, ParseResult result) { return new RefResponse(obj.asText()); } @@ -676,21 +741,20 @@ public Path pathRef(TextNode ref, String location, ParseResult result) { return output; } - public Map definitions (ObjectNode node, String location, ParseResult result) { - if(node == null) + public Map definitions(ObjectNode node, String location, ParseResult result) { + if (node == null) return null; Set schemas = getKeys(node); Map output = new LinkedHashMap(); - for(String schemaName : schemas) { + for (String schemaName : schemas) { JsonNode schema = node.get(schemaName); - if(schema.getNodeType().equals(JsonNodeType.OBJECT)) { + if (schema.getNodeType().equals(JsonNodeType.OBJECT)) { Model model = definition((ObjectNode) schema, location + "." + schemaName, result); - if(model != null) { + if (model != null) { output.put(schemaName, model); } - } - else { + } else { result.invalidType(location, schemaName, "object", schema); } } @@ -698,35 +762,38 @@ public Map definitions (ObjectNode node, String location, ParseRe } public Model definition(ObjectNode node, String location, ParseResult result) { - if(result == null) { + if (result == null) { // TODO, this shouldn't happen, but the `ResolverCache#loadRef` method is passing null result = new ParseResult(); } - if(node == null) { + if (node == null) { result.missing(location, "empty schema"); return null; } - if(node.get("$ref") != null) { + if (node.isBoolean()) { + return new BooleanValueModel(node.asBoolean()); + } + if (node.get("$ref") != null) { return refModel(node, location, result); } - if(node.get("allOf") != null) { + if (node.get("allOf") != null) { return allOfModel(node, location, result); } Model model = null; String value = null; String type = getString("type", node, false, location, result); + JsonNode itemsKey = node.get("items"); Model m = new ModelImpl(); - if("array".equals(type)) { + if ("array".equals(type) || itemsKey != null) { ArrayModel am = new ArrayModel(); ObjectNode propertyNode = getObject("properties", node, false, location, result); Map properties = properties(propertyNode, location, result); am.setProperties(properties); - ObjectNode itemsNode = getObject("items", node, false, location, result); Property items = property(itemsNode, location, result); - if(items != null) { + if (items != null) { am.items(items); } @@ -736,23 +803,35 @@ public Model definition(ObjectNode node, String location, ParseResult result) { Integer minItems = getInteger("minItems", node, false, location, result); am.setMinItems(minItems); + Boolean uniqueItems = getBoolean("uniqueItems", node, false, location, result); + if (uniqueItems != null) { + am.setUniqueItems(uniqueItems); + } + + // add xml specific information if available + JsonNode xml = node.get("xml"); + if (xml != null) { + am.setXml(Json.mapper().convertValue(xml, Xml.class)); + } + // extra keys Set keys = getKeys(node); - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { am.setVendorExtension(key, extension(node.get(key))); } } model = am; - } - else { + } else { ModelImpl impl = new ModelImpl(); impl.setType(type); JsonNode ap = node.get("additionalProperties"); - if(ap != null && ap.getNodeType().equals(JsonNodeType.OBJECT)) { + if (ap != null && ap.getNodeType().equals(JsonNodeType.OBJECT)) { impl.setAdditionalProperties(Json.mapper().convertValue(ap, Property.class)); + } else if (ap != null && ap.isBoolean()) { + impl.setAdditionalProperties(new BooleanValueProperty(ap.asBoolean())); } value = getString("default", node, false, location, result); @@ -765,17 +844,24 @@ public Model definition(ObjectNode node, String location, ParseResult result) { impl.setDiscriminator(value); Boolean bp = getBoolean("uniqueItems", node, false, location, result); - if(bp != null) { + if (bp != null) { impl.setUniqueItems(bp); } + + BigDecimal bd = getBigDecimal("minimum", node, false, location, result); + impl.setMinimum(bd); + + bd = getBigDecimal("maximum", node, false, location, result); + impl.setMaximum(bd); + bp = getBoolean("exclusiveMaximum", node, false, location, result); - if(bp != null) { + if (bp != null) { impl.setExclusiveMaximum(bp); } bp = getBoolean("exclusiveMinimum", node, false, location, result); - if(bp != null) { + if (bp != null) { impl.setExclusiveMinimum(bp); } @@ -783,39 +869,39 @@ public Model definition(ObjectNode node, String location, ParseResult result) { impl.setPattern(value); BigDecimal maximum = getBigDecimal("maximum", node, false, location, result); - if(maximum != null) { + if (maximum != null) { impl.maximum(maximum); } BigDecimal minimum = getBigDecimal("minimum", node, false, location, result); - if(minimum != null) { + if (minimum != null) { impl.minimum(minimum); } Integer minLength = getInteger("minLength", node, false, location, result); - if(minLength != null) { + if (minLength != null) { impl.setMinLength(minLength); } Integer maxLength = getInteger("maxLength", node, false, location, result); - if(maxLength != null) { + if (maxLength != null) { impl.setMaxLength(maxLength); } BigDecimal multipleOf = getBigDecimal("multipleOf", node, false, location, result); - if(multipleOf != null) { + if (multipleOf != null) { impl.setMultipleOf(multipleOf); } + ap = node.get("enum"); - if(ap != null) { + if (ap != null) { ArrayNode arrayNode = getArray("enum", node, false, location, result); - if(arrayNode != null) { - for(JsonNode n : arrayNode) { - if(n.isValueNode()) { + if (arrayNode != null) { + for (JsonNode n : arrayNode) { + if (n.isValueNode()) { impl._enum(n.asText()); - } - else { + } else { result.invalidType(location, "enum", "value", n); } } @@ -823,7 +909,7 @@ public Model definition(ObjectNode node, String location, ParseResult result) { } JsonNode xml = node.get("xml"); - if(xml != null) { + if (xml != null) { impl.setXml(Json.mapper().convertValue(xml, Xml.class)); } @@ -831,58 +917,42 @@ public Model definition(ObjectNode node, String location, ParseResult result) { ExternalDocs docs = externalDocs(externalDocs, location, result); impl.setExternalDocs(docs); - ObjectNode properties = getObject("properties", node, false, location, result); - if(properties != null) { - Set propertyNames = getKeys(properties); - for(String propertyName : propertyNames) { - JsonNode propertyNode = properties.get(propertyName); - if(propertyNode.getNodeType().equals(JsonNodeType.OBJECT)) { - ObjectNode on = (ObjectNode) propertyNode; - Property property = property(on, location, result); - impl.property(propertyName, property); - } - else { - result.invalidType(location, "properties", "object", propertyNode); - } - } - } + addProperties(location, node, result, impl); // need to set properties first ArrayNode required = getArray("required", node, false, location, result); - if(required != null) { + if (required != null) { List requiredProperties = new ArrayList(); for (JsonNode n : required) { - if(n.getNodeType().equals(JsonNodeType.STRING)) { + if (n.getNodeType().equals(JsonNodeType.STRING)) { requiredProperties.add(((TextNode) n).textValue()); - } - else { + } else { result.invalidType(location, "required", "string", n); } } - if(requiredProperties.size() > 0) { + if (requiredProperties.size() > 0) { impl.setRequired(requiredProperties); } } // extra keys Set keys = getKeys(node); - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { impl.setVendorExtension(key, extension(node.get(key))); - } - else if(!SCHEMA_KEYS.contains(key)) { + } else if (!SCHEMA_KEYS.contains(key)) { result.extra(location, key, node.get(key)); } } model = impl; } JsonNode exampleNode = node.get("example"); - if(exampleNode != null) { + if (exampleNode != null) { Object example = Json.mapper().convertValue(exampleNode, Object.class); model.setExample(example); } - if(model != null) { + if (model != null) { value = getString("description", node, false, location, result); model.setDescription(value); @@ -902,27 +972,25 @@ public Model allOfModel(ObjectNode node, String location, ParseResult result) { JsonNode allOf = node.get("allOf"); if (sub != null) { - if(sub.getNodeType().equals(JsonNodeType.OBJECT)) { - return refModel((ObjectNode)sub, location, result); - } - else { + if (sub.getNodeType().equals(JsonNodeType.OBJECT)) { + return refModel((ObjectNode) sub, location, result); + } else { result.invalidType(location, "$ref", "object", sub); return null; } } else if (allOf != null) { ComposedModel model = null; - if(allOf.getNodeType().equals(JsonNodeType.ARRAY)) { + if (allOf.getNodeType().equals(JsonNodeType.ARRAY)) { model = new ComposedModel(); int pos = 0; - for(JsonNode part : allOf) { - if(part.getNodeType().equals(JsonNodeType.OBJECT)) { + for (JsonNode part : allOf) { + if (part.getNodeType().equals(JsonNodeType.OBJECT)) { Model segment = definition((ObjectNode) part, location, result); - if(segment != null) { + if (segment != null) { model.getAllOf().add(segment); } - } - else { + } else { result.invalidType(location, "allOf[" + pos + "]", "object", part); } pos++; @@ -940,25 +1008,22 @@ public Model allOfModel(ObjectNode node, String location, ParseResult result) { } model.setInterfaces(interfaces); - if(child != null) { + if (child != null) { model.setChild(child); } - } - else { + } else { result.invalidType(location, "allOf", "array", allOf); } // extra keys Set keys = getKeys(node); - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { model.setVendorExtension(key, extension(node.get(key))); - } - else if(!SCHEMA_KEYS.contains(key)) { + } else if (!SCHEMA_KEYS.contains(key)) { result.extra(location, key, node.get(key)); - } - else { + } else { String value = getString("title", node, false, location, result); model.setTitle(value); @@ -967,25 +1032,64 @@ else if(!SCHEMA_KEYS.contains(key)) { } } + addProperties(location, node, result, model); + + // need to set properties first + ArrayNode required = getArray("required", node, false, location, result); + if (required != null) { + List requiredProperties = new ArrayList(); + for (JsonNode n : required) { + if (n.getNodeType().equals(JsonNodeType.STRING)) { + requiredProperties.add(((TextNode) n).textValue()); + } else { + result.invalidType(location, "required", "string", n); + } + } + if (requiredProperties.size() > 0) { + model.setRequired(requiredProperties); + } + } + return model; } return null; } + private void addProperties(String location, ObjectNode node, ParseResult result, AbstractModel model) { + ObjectNode properties = getObject("properties", node, false, location, result); + if (properties != null) { + Set propertyNames = getKeys(properties); + for (String propertyName : propertyNames) { + JsonNode propertyNode = properties.get(propertyName); + if (propertyNode.getNodeType().equals(JsonNodeType.OBJECT)) { + ObjectNode on = (ObjectNode) propertyNode; + Property property = property(on, location, result); + if (property != null) { + if ("array".equals(property.getType()) && !(property instanceof ArrayProperty && ((ArrayProperty) property).getItems() != null)) { + result.missing(location, "items"); + } + } + model.addProperty(propertyName, property); + } else { + result.invalidType(location, "properties", "object", propertyNode); + } + } + } + } + public Map properties(ObjectNode node, String location, ParseResult result) { - if(node == null) { + if (node == null) { return null; } Map output = new LinkedHashMap(); Set keys = getKeys(node); - for(String propertyName : keys) { + for (String propertyName : keys) { JsonNode propertyNode = node.get(propertyName); - if(propertyNode.getNodeType().equals(JsonNodeType.OBJECT)) { - Property property = property((ObjectNode)propertyNode, location, result); + if (propertyNode.getNodeType().equals(JsonNodeType.OBJECT)) { + Property property = property((ObjectNode) propertyNode, location, result); output.put(propertyName, property); - } - else { + } else { result.invalidType(location, propertyName, "object", propertyNode); } } @@ -993,11 +1097,14 @@ public Map properties(ObjectNode node, String location, ParseR } public Property property(ObjectNode node, String location, ParseResult result) { - if(node != null) { - if(node.get("type") == null) { + if (node != null) { + if (node.isBoolean()) { + return new BooleanValueProperty(node.asBoolean()); + } + if (node.get("type") == null) { // may have an enum where type can be inferred JsonNode enumNode = node.get("enum"); - if(enumNode != null && enumNode.isArray()) { + if (enumNode != null && enumNode.isArray()) { String type = inferTypeFromArray((ArrayNode) enumNode); node.put("type", type); } @@ -1008,29 +1115,25 @@ public Property property(ObjectNode node, String location, ParseResult result) { } public String inferTypeFromArray(ArrayNode an) { - if(an.size() == 0) { + if (an.size() == 0) { return "string"; } String type = null; - for(int i = 0; i < an.size(); i++) { + for (int i = 0; i < an.size(); i++) { JsonNode element = an.get(0); - if(element.isBoolean()) { - if(type == null) { + if (element.isBoolean()) { + if (type == null) { type = "boolean"; - } - else if(!"boolean".equals(type)) { + } else if (!"boolean".equals(type)) { type = "string"; } - } - else if(element.isNumber()) { - if(type == null) { + } else if (element.isNumber()) { + if (type == null) { type = "number"; - } - else if(!"number".equals(type)) { + } else if (!"number".equals(type)) { type = "string"; } - } - else { + } else { type = "string"; } } @@ -1041,19 +1144,18 @@ else if(!"number".equals(type)) { public RefModel refModel(ObjectNode node, String location, ParseResult result) { RefModel output = new RefModel(); - if(node.getNodeType().equals(JsonNodeType.OBJECT)) { - String refValue = ((TextNode)node.get("$ref")).textValue(); + if (node.getNodeType().equals(JsonNodeType.OBJECT)) { + String refValue = ((TextNode) node.get("$ref")).textValue(); output.set$ref(refValue); - } - else { + } else { result.invalidType(location, "$ref", "object", node); return null; } // extra keys Set keys = getKeys(node); - for(String key : keys) { - if(!REF_MODEL_KEYS.contains(key)) { + for (String key : keys) { + if (!REF_MODEL_KEYS.contains(key)) { result.extra(location, key, node.get(key)); } } @@ -1061,17 +1163,19 @@ public RefModel refModel(ObjectNode node, String location, ParseResult result) { return output; } - public Map responses(ObjectNode node, String location, ParseResult result) { + + public Responses responses(ObjectNode node, String location, ParseResult result) { if(node == null) return null; - Map output = new TreeMap(); + Responses output = new Responses(); Set keys = getKeys(node); for(String key : keys) { + JsonNode responseValue = node.get(key); if (key.startsWith("x-")) { - + output.addVendorExtension(key,extension(responseValue)); } else { ObjectNode obj = getObject(key, node, false, location + ".responses", result); @@ -1084,17 +1188,16 @@ public Map responses(ObjectNode node, String location, ParseRe } public Response response(ObjectNode node, String location, ParseResult result) { - if(node == null) + if (node == null) return null; Response output = new Response(); JsonNode ref = node.get("$ref"); - if(ref != null) { - if(ref.getNodeType().equals(JsonNodeType.STRING)) { + if (ref != null) { + if (ref.getNodeType().equals(JsonNodeType.STRING)) { return refResponse((TextNode) ref, location, result); - } - else { + } else { result.invalidType(location, "$ref", "string", node); return null; } @@ -1104,7 +1207,7 @@ public Response response(ObjectNode node, String location, ParseResult result) { output.description(value); ObjectNode schema = getObject("schema", node, false, location, result); - if(schema != null) { + if (schema != null) { JsonNode schemaRef = schema.get("$ref"); if (schemaRef != null) { if (schemaRef.getNodeType().equals(JsonNodeType.STRING)) { @@ -1114,12 +1217,12 @@ public Response response(ObjectNode node, String location, ParseResult result) { result.invalidType(location, "$ref", "string", node); } } else { - output.responseSchema(Json.mapper().convertValue(schema, Model.class)); + output.responseSchema(definition(schema, location + ".schema", result)); } } ObjectNode headersNode = getObject("headers", node, false, location, result); - if(headersNode != null) { + if (headersNode != null) { // TODO Map headers = Json.mapper().convertValue(headersNode, Json.mapper().getTypeFactory().constructMapType(Map.class, String.class, Property.class)); @@ -1127,18 +1230,17 @@ public Response response(ObjectNode node, String location, ParseResult result) { } ObjectNode examplesNode = getObject("examples", node, false, location, result); - if(examplesNode != null) { + if (examplesNode != null) { Map examples = Json.mapper().convertValue(examplesNode, Json.mapper().getTypeFactory().constructMapType(Map.class, String.class, Object.class)); output.setExamples(examples); } // extra keys Set keys = getKeys(node); - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { output.setVendorExtension(key, extension(node.get(key))); - } - else if(!RESPONSE_KEYS.contains(key)) { + } else if (!RESPONSE_KEYS.contains(key)) { result.extra(location, key, node.get(key)); } } @@ -1146,7 +1248,7 @@ else if(!RESPONSE_KEYS.contains(key)) { } public Info info(ObjectNode node, String location, ParseResult result) { - if(node == null) + if (node == null) return null; Info info = new Info(); @@ -1167,16 +1269,15 @@ public Info info(ObjectNode node, String location, ParseResult result) { License license = license(obj, location, result); info.license(license); - value = getString("version", node, false, location, result); + value = getString("version", node, true, location, result); info.version(value); // extra keys Set keys = getKeys(node); - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { info.setVendorExtension(key, extension(node.get(key))); - } - else if(!INFO_KEYS.contains(key)) { + } else if (!INFO_KEYS.contains(key)) { result.extra(location, key, node.get(key)); } } @@ -1185,7 +1286,7 @@ else if(!INFO_KEYS.contains(key)) { } public License license(ObjectNode node, String location, ParseResult result) { - if(node == null) + if (node == null) return null; License license = new License(); @@ -1198,11 +1299,10 @@ public License license(ObjectNode node, String location, ParseResult result) { // extra keys Set keys = getKeys(node); - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { license.setVendorExtension(key, extension(node.get(key))); - } - else if(!LICENSE_KEYS.contains(key)) { + } else if (!LICENSE_KEYS.contains(key)) { result.extra(location + ".license", key, node.get(key)); } } @@ -1211,7 +1311,7 @@ else if(!LICENSE_KEYS.contains(key)) { } public Contact contact(ObjectNode node, String location, ParseResult result) { - if(node == null) + if (node == null) return null; Contact contact = new Contact(); @@ -1227,8 +1327,10 @@ public Contact contact(ObjectNode node, String location, ParseResult result) { // extra keys Set keys = getKeys(node); - for(String key : keys) { - if(!CONTACT_KEYS.contains(key)) { + for (String key : keys) { + if (key.startsWith("x-")) { + contact.setVendorExtension(key, extension(node.get(key))); + } else if (!CONTACT_KEYS.contains(key)) { result.extra(location + ".contact", key, node.get(key)); } } @@ -1237,17 +1339,17 @@ public Contact contact(ObjectNode node, String location, ParseResult result) { } public Map securityDefinitions(ObjectNode node, String location, ParseResult result) { - if(node == null) + if (node == null) return null; Map output = new LinkedHashMap<>(); Set keys = getKeys(node); - for(String key : keys) { + for (String key : keys) { ObjectNode obj = getObject(key, node, false, location, result); SecuritySchemeDefinition def = securityDefinition(obj, location + "." + key, result); - if(def != null) { + if (def != null) { output.put(key, def); } } @@ -1256,24 +1358,23 @@ public Map securityDefinitions(ObjectNode node } public SecuritySchemeDefinition securityDefinition(ObjectNode node, String location, ParseResult result) { - if(node == null) + if (node == null) return null; SecuritySchemeDefinition output = null; String type = getString("type", node, true, location, result); - if(type != null) { - if(type.equals("basic")) { + if (type != null) { + if (type.equals("basic")) { // TODO: parse manually for better feedback output = Json.mapper().convertValue(node, BasicAuthDefinition.class); - } - else if (type.equals("apiKey")) { + } else if (type.equals("apiKey")) { String position = getString("in", node, true, location, result); String name = getString("name", node, true, location, result); - if(name != null && ("header".equals(position) || "query".equals(position))) { + if (name != null && ("header".equals(position) || "query".equals(position))) { In in = In.forValue(position); - if(in != null) { + if (in != null) { output = new ApiKeyAuthDefinition() .name(name) .in(in); @@ -1282,29 +1383,26 @@ else if (type.equals("apiKey")) { } } JsonNode desc = node.get("description"); - if(desc != null) { + if (desc != null) { output.setDescription(desc.textValue()); } - } - else if (type.equals("oauth2")) { + } else if (type.equals("oauth2")) { // TODO: parse manually for better feedback output = Json.mapper().convertValue(node, OAuth2Definition.class); JsonNode desc = node.get("description"); - if(desc != null) { + if (desc != null) { output.setDescription(desc.textValue()); } - } - else { + } else { result.invalidType(location + ".type", "type", "basic|apiKey|oauth2", node); } - + // extra keys Set keys = getKeys(node); - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { output.setVendorExtension(key, extension(node.get(key))); - } - else if(!SECURITY_SCHEME_KEYS.contains(key)) { + } else if (!SECURITY_SCHEME_KEYS.contains(key)) { result.extra(location, key, node.get(key)); } } @@ -1315,14 +1413,14 @@ else if(!SECURITY_SCHEME_KEYS.contains(key)) { } public List securityRequirements(ArrayNode node, String location, ParseResult result) { - if(node == null) + if (node == null) return null; List output = new ArrayList(); - for(JsonNode item : node) { + for (JsonNode item : node) { SecurityRequirement security = new SecurityRequirement(); - if(item.getNodeType().equals(JsonNodeType.OBJECT)) { + if (item.getNodeType().equals(JsonNodeType.OBJECT)) { ObjectNode on = (ObjectNode) item; Set keys = getKeys(on); @@ -1349,13 +1447,13 @@ public List securityRequirements(ArrayNode node, String loc public List tagStrings(ArrayNode nodes, String location, ParseResult result) { - if(nodes == null) + if (nodes == null) return null; List output = new ArrayList(); - for(JsonNode node : nodes) { - if(node.getNodeType().equals(JsonNodeType.STRING)) { + for (JsonNode node : nodes) { + if (node.getNodeType().equals(JsonNodeType.STRING)) { output.add(node.textValue()); } } @@ -1363,15 +1461,15 @@ public List tagStrings(ArrayNode nodes, String location, ParseResult res } public List tags(ArrayNode nodes, String location, ParseResult result) { - if(nodes == null) + if (nodes == null) return null; List output = new ArrayList(); - for(JsonNode node : nodes) { - if(node.getNodeType().equals(JsonNodeType.OBJECT)) { + for (JsonNode node : nodes) { + if (node.getNodeType().equals(JsonNodeType.OBJECT)) { Tag tag = tag((ObjectNode) node, location + ".tags", result); - if(tag != null) { + if (tag != null) { output.add(tag); } } @@ -1383,7 +1481,7 @@ public List tags(ArrayNode nodes, String location, ParseResult result) { public Tag tag(ObjectNode node, String location, ParseResult result) { Tag tag = null; - if(node != null) { + if (node != null) { tag = new Tag(); Set keys = getKeys(node); @@ -1398,11 +1496,10 @@ public Tag tag(ObjectNode node, String location, ParseResult result) { tag.externalDocs(docs); // extra keys - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { tag.setVendorExtension(key, extension(node.get(key))); - } - else if(!TAG_KEYS.contains(key)) { + } else if (!TAG_KEYS.contains(key)) { result.extra(location + ".externalDocs", key, node.get(key)); } } @@ -1414,7 +1511,7 @@ else if(!TAG_KEYS.contains(key)) { public ExternalDocs externalDocs(ObjectNode node, String location, ParseResult result) { ExternalDocs output = null; - if(node != null) { + if (node != null) { output = new ExternalDocs(); Set keys = getKeys(node); @@ -1425,11 +1522,10 @@ public ExternalDocs externalDocs(ObjectNode node, String location, ParseResult r output.url(value); // extra keys - for(String key : keys) { - if(key.startsWith("x-")) { + for (String key : keys) { + if (key.startsWith("x-")) { output.setVendorExtension(key, extension(node.get(key))); - } - else if(!EXTERNAL_DOCS_KEYS.contains(key)) { + } else if (!EXTERNAL_DOCS_KEYS.contains(key)) { result.extra(location + ".externalDocs", key, node.get(key)); } } @@ -1440,10 +1536,9 @@ else if(!EXTERNAL_DOCS_KEYS.contains(key)) { public String getString(JsonNode node, String location, ParseResult result) { String output = null; - if(!node.getNodeType().equals(JsonNodeType.STRING)) { + if (!node.getNodeType().equals(JsonNodeType.STRING)) { result.invalidType(location, "", "string", node); - } - else { + } else { output = ((TextNode) node).asText(); } return output; @@ -1452,16 +1547,14 @@ public String getString(JsonNode node, String location, ParseResult result) { public ArrayNode getArray(String key, ObjectNode node, boolean required, String location, ParseResult result) { JsonNode value = node.get(key); ArrayNode an = null; - if(value == null) { - if(required) { + if (value == null) { + if (required) { result.missing(location, key); result.invalid(); } - } - else if(!value.getNodeType().equals(JsonNodeType.ARRAY)) { + } else if (!value.getNodeType().equals(JsonNodeType.ARRAY)) { result.invalidType(location, key, "array", value); - } - else { + } else { an = (ArrayNode) value; } return an; @@ -1470,19 +1563,17 @@ else if(!value.getNodeType().equals(JsonNodeType.ARRAY)) { public ObjectNode getObject(String key, ObjectNode node, boolean required, String location, ParseResult result) { JsonNode value = node.get(key); ObjectNode on = null; - if(value == null) { - if(required) { + if (value == null) { + if (required) { result.missing(location, key); result.invalid(); } - } - else if(!value.getNodeType().equals(JsonNodeType.OBJECT)) { + } else if (!value.getNodeType().equals(JsonNodeType.OBJECT)) { result.invalidType(location, key, "object", value); - if(required) { + if (required) { result.invalid(); } - } - else { + } else { on = (ObjectNode) value; } return on; @@ -1496,11 +1587,9 @@ public BigDecimal getBigDecimal(String key, ObjectNode node, boolean required, S result.missing(location, key); result.invalid(); } - } - else if(v.getNodeType().equals(JsonNodeType.NUMBER)) { + } else if (v.getNodeType().equals(JsonNodeType.NUMBER)) { value = new BigDecimal(v.asText()); - } - else if(!v.isValueNode()) { + } else if (!v.isValueNode()) { result.invalidType(location, key, "double", node); } return value; @@ -1514,11 +1603,9 @@ public Number getNumber(String key, ObjectNode node, boolean required, String lo result.missing(location, key); result.invalid(); } - } - else if(v.getNodeType().equals(JsonNodeType.NUMBER)) { + } else if (v.getNodeType().equals(JsonNodeType.NUMBER)) { value = v.numberValue(); - } - else if(!v.isValueNode()) { + } else if (!v.isValueNode()) { result.invalidType(location, key, "number", node); } return value; @@ -1532,17 +1619,19 @@ public Integer getInteger(String key, ObjectNode node, boolean required, String result.missing(location, key); result.invalid(); } - } - else if(v.getNodeType().equals(JsonNodeType.NUMBER)) { + } else if (v.getNodeType().equals(JsonNodeType.NUMBER)) { value = v.intValue(); - } - else if(!v.isValueNode()) { + } else if (!v.isValueNode()) { result.invalidType(location, key, "integer", node); } return value; } public String getString(String key, ObjectNode node, boolean required, String location, ParseResult result) { + return getString(key, node, required, location, result, null); + } + + public String getString(String key, ObjectNode node, boolean required, String location, ParseResult result, Set uniqueValues) { String value = null; JsonNode v = node.get(key); if (node == null || v == null) { @@ -1550,19 +1639,21 @@ public String getString(String key, ObjectNode node, boolean required, String lo result.missing(location, key); result.invalid(); } - } - else if(!v.isValueNode()) { + } else if (!v.isValueNode()) { result.invalidType(location, key, "string", node); - } - else { + } else { value = v.asText(); + if (uniqueValues != null && !uniqueValues.add(value)) { + result.unique(location, "operationId"); + result.invalid(); + } } return value; } public Set getKeys(ObjectNode node) { Set keys = new LinkedHashSet<>(); - if(node == null) { + if (node == null) { return keys; } @@ -1579,7 +1670,9 @@ protected static class ParseResult { private Map extra = new LinkedHashMap(); private Map unsupported = new LinkedHashMap(); private Map invalidType = new LinkedHashMap(); + private List warnings = new ArrayList<>(); private List missing = new ArrayList(); + private List unique = new ArrayList<>(); public ParseResult() { } @@ -1592,11 +1685,19 @@ public void extra(String location, String key, JsonNode value) { extra.put(new Location(location, key), value); } + public void unique(String location, String key) { + unique.add(new Location(location, key)); + } + public void missing(String location, String key) { missing.add(new Location(location, key)); } - public void invalidType(String location, String key, String expectedType, JsonNode value){ + public void warning(String location, String key) { + warnings.add(new Location(location, key)); + } + + public void invalidType(String location, String key, String expectedType, JsonNode value) { invalidType.put(new Location(location, key), expectedType); } @@ -1646,22 +1747,32 @@ public void setMissing(List missing) { public List getMessages() { List messages = new ArrayList(); - for(Location l : extra.keySet()) { + for (Location l : extra.keySet()) { String location = l.location.equals("") ? "" : l.location + "."; String message = "attribute " + location + l.key + " is unexpected"; messages.add(message); } - for(Location l : invalidType.keySet()) { + for (Location l : invalidType.keySet()) { String location = l.location.equals("") ? "" : l.location + "."; String message = "attribute " + location + l.key + " is not of type `" + invalidType.get(l) + "`"; messages.add(message); } - for(Location l : missing) { + for (Location l : missing) { String location = l.location.equals("") ? "" : l.location + "."; String message = "attribute " + location + l.key + " is missing"; messages.add(message); } - for(Location l : unsupported.keySet()) { + for (Location l : warnings) { + String location = l.location.equals("") ? "" : l.location + "."; + String message = "attribute " + location + l.key; + messages.add(message); + } + for (Location l : unique) { + String location = l.location.equals("") ? "" : l.location + "."; + String message = "attribute " + location + l.key + " is repeated"; + messages.add(message); + } + for (Location l : unsupported.keySet()) { String location = l.location.equals("") ? "" : l.location + "."; String message = "attribute " + location + l.key + " is unsupported"; messages.add(message); @@ -1693,6 +1804,7 @@ public int hashCode() { } private String key; + public Location(String location, String key) { this.location = location; this.key = key; diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/AnchorTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/AnchorTest.java index c48a61811a..eabeb005d0 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/AnchorTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/AnchorTest.java @@ -1,14 +1,21 @@ package io.swagger.parser; import io.swagger.models.ModelImpl; +import io.swagger.models.Swagger; +import io.swagger.models.properties.StringProperty; +import io.swagger.parser.util.DeserializationUtils; import io.swagger.parser.util.SwaggerDeserializationResult; -import org.testng.annotations.Test; +import io.swagger.util.Json; +import org.junit.Test; import java.util.Arrays; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; public class AnchorTest { + @Test public void testIssue146() { String yaml = "swagger: '2.0'\n" + @@ -47,4 +54,85 @@ public void testIssue146() { ModelImpl model = (ModelImpl) result.getSwagger().getDefinitions().get("OperationType"); assertEquals(model.getEnum(), Arrays.asList("registration")); } + + @Test + public void testCycle() { + + String yaml = "a:\n" + + " a1: &a1\n" + + " a2: \n" + + " - a3\n" + + " - a4\n" + + " a5: \n" + + " - *a1"; + + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo(yaml); + assertEquals(Json.pretty(result.getSwagger()), "{ }"); + + } + + @org.junit.Test + public void testIssue998() throws Exception{ + + //DeserializationUtils.getOptions().setMaxYamlDepth(5000); + Swagger result = new SwaggerParser().read("issue_998.yaml"); + assertNull(result); + + } + + @org.junit.Test + public void testIssue998Billion() throws Exception{ + DeserializationUtils.getOptions().setMaxYamlReferences(100000L); + String yaml = "a: &a [\"lol\",\"lol\",\"lol\",\"lol\",\"lol\",\"lol\",\"lol\",\"lol\",\"lol\"]\n" + + "b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]\n" + + "c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]\n" + + "d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]\n" + + "e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]\n" + + "f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]\n" + + "g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]\n" + + "h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]\n" + + "i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]"; + + String yaml2 = "a: &a [\"lol\",\"lol\",\"lol\",\"lol\",\"lol\",\"lol\",\"lol\",\"lol\",\"lol\"]\n" + + "b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]\n" + + "c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]\n" + + "d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]\n" + + "e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]\n" + + "f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]\n" + + "g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]\n" + + "h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]"; + + String yaml3 = "a: &a [\"lol\"]\n" + + "b: &b [*a,*a]\n" + + "c: &c [*b,*b]"; + + Swagger result = new SwaggerParser().readWithInfo(yaml).getSwagger(); + assertEquals(Json.pretty(result), "{ }"); + result = new SwaggerParser().readWithInfo(yaml2).getSwagger(); + assertEquals(Json.pretty(result), "{ }"); + DeserializationUtils.getOptions().setMaxYamlReferences(10000000L); + + } + + @org.testng.annotations.Test + public void testBillionLaughProtectionSnakeYaml() { + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("billion_laughs_snake_yaml.yaml", null, true); + + assertNotNull(result.getSwagger().getDefinitions().get("a1")); + assertEquals(((ModelImpl)result.getSwagger().getDefinitions().get("a1")).getEnum().get(0), "AA1"); + assertNotNull(result.getSwagger().getDefinitions().get("c1")); + assertEquals(((StringProperty)result.getSwagger().getDefinitions().get("c1").getProperties().get("a")).getEnum().get(0), "AA1"); + + + DeserializationUtils.getOptions().setMaxYamlAliasesForCollections(50); + DeserializationUtils.getOptions().setYamlAllowRecursiveKeys(false); + + result = new SwaggerParser().readWithInfo("billion_laughs_snake_yaml.yaml", null, true); + + assertNull(result.getSwagger()); + DeserializationUtils.getOptions().setMaxYamlAliasesForCollections(Integer.MAX_VALUE); + DeserializationUtils.getOptions().setYamlAllowRecursiveKeys(true); + + } + } diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/FileReferenceTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/FileReferenceTest.java index c7607bbf8f..578ba682c7 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/FileReferenceTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/FileReferenceTest.java @@ -117,6 +117,29 @@ public void testIssue316() { assertEquals(ref.get$ref(), "#/definitions/Foobar"); } + @Test + public void testIssue1223() { + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("./src/test/resources/nested-file-references/issue-1223.yaml", null, true); + assertNotNull(result.getSwagger()); + + Swagger swagger = result.getSwagger(); + assertNotNull(swagger.getPath("/events")); + Path path = swagger.getPath("/events"); + assertNotNull(path.getGet()); + Operation get = path.getGet(); + assertEquals(get.getOperationId(), "getEvents"); + assertEquals(swagger.getDefinitions().size(),3); + assertEquals(swagger.getDefinitions().get("Foobar").getProperties().size(), 1); + assertEquals(swagger.getDefinitions().get("StatusResponse").getProperties().size(), 1); + assertEquals(swagger.getDefinitions().get("Paging2").getProperties().size(), 2); + Model model = swagger.getDefinitions().get("Paging2"); + + Property property = model.getProperties().get("foobar"); + assertTrue(property instanceof RefProperty); + RefProperty ref = (RefProperty) property; + assertEquals(ref.get$ref(), "#/definitions/Foobar"); + } + @Test public void testIssue323() { SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("./src/test/resources/nested-file-references/issue-323.yaml", null, true); @@ -236,12 +259,12 @@ public void testRelativeRefIssue421() { assertNotNull(swagger); assertNotNull(swagger.getPath("pets")); assertNotNull(swagger.getPath("pets").getGet()); - assertNotNull(swagger.getPath("pets").getGet().getResponses()); - assertNotNull(swagger.getPath("pets").getGet().getResponses().get("200")); - assertNotNull(swagger.getPath("pets").getGet().getResponses().get("200").getSchema()); - assertTrue(swagger.getPath("pets").getGet().getResponses().get("200").getSchema() instanceof RefProperty); + assertNotNull(swagger.getPath("pets").getGet().getResponsesObject()); + assertNotNull(swagger.getPath("pets").getGet().getResponsesObject().get("200")); + assertNotNull(swagger.getPath("pets").getGet().getResponsesObject().get("200").getResponseSchema()); + assertTrue(swagger.getPath("pets").getGet().getResponsesObject().get("200").getResponseSchema() instanceof RefModel); - assertEquals(((RefProperty)swagger.getPath("pets").getGet().getResponses().get("200").getSchema()).get$ref(), "#/definitions/Pet"); + assertEquals(((RefModel)swagger.getPath("pets").getGet().getResponsesObject().get("200").getResponseSchema()).get$ref(), "#/definitions/Pet"); assertTrue(swagger.getDefinitions().get("Pet") instanceof ModelImpl); assertTrue(swagger.getDefinitions().get("Pet").getProperties().size() == 2); diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/NetworkReferenceTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/NetworkReferenceTest.java index 72dc2e2551..0a8a66f3c8 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/NetworkReferenceTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/NetworkReferenceTest.java @@ -2,6 +2,7 @@ import io.swagger.models.ModelImpl; import io.swagger.models.Path; +import io.swagger.models.RefModel; import io.swagger.models.Response; import io.swagger.models.Swagger; import io.swagger.models.auth.AuthorizationValue; @@ -218,10 +219,10 @@ public void testIssue411() throws Exception { assertNotNull(swagger.getPath("/health")); Path health = swagger.getPath("/health"); assertTrue(health.getGet().getParameters().size() == 0); - Object responseRef = health.getGet().getResponses().get("200").getSchema(); - assertTrue(responseRef instanceof RefProperty); + Object responseRef = health.getGet().getResponsesObject().get("200").getResponseSchema(); + assertTrue(responseRef instanceof RefModel); - RefProperty refProperty = (RefProperty) responseRef; + RefModel refProperty = (RefModel) responseRef; assertEquals(refProperty.get$ref(), "#/definitions/Success"); assertNotNull(swagger.getDefinitions().get("Success")); @@ -230,11 +231,11 @@ public void testIssue411() throws Exception { assertEquals(param.getIn(), "query"); assertEquals(param.getName(), "skip"); - Response response = swagger.getPath("/stuff").getGet().getResponses().get("200"); + Response response = swagger.getPath("/stuff").getGet().getResponsesObject().get("200"); assertNotNull(response); assertTrue(response.getSchema() instanceof StringProperty); - Response error = swagger.getPath("/stuff").getGet().getResponses().get("400"); + Response error = swagger.getPath("/stuff").getGet().getResponsesObject().get("400"); assertNotNull(error); Property errorProp = error.getSchema(); assertNotNull(errorProp); diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/ResolverCacheTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/ResolverCacheTest.java index dd96b781a1..62082fea07 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/ResolverCacheTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/ResolverCacheTest.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; +import io.swagger.models.Responses; import org.apache.commons.lang3.tuple.Pair; import org.testng.annotations.Test; @@ -184,7 +185,7 @@ public void testLoadInternalDefinitionRefWithEscapedCharacters(@Injectable Model @Test public void testLoadInternalResponseRef(@Injectable Response mockedResponse) throws Exception { Swagger swagger = new Swagger(); - Map responses = new HashMap<>(); + Responses responses = new Responses(); responses.put("foo", mockedResponse); swagger.setResponses(responses); @@ -198,7 +199,7 @@ public void testLoadInternalResponseRef(@Injectable Response mockedResponse) thr @Test public void testLoadInternalResponseRefWithSpaces(@Injectable Response mockedResponse) throws Exception { Swagger swagger = new Swagger(); - Map responses = new HashMap<>(); + Responses responses = new Responses(); responses.put("foo bar", mockedResponse); swagger.setResponses(responses); diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerParserTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerParserTest.java index 3dccc5e671..1fb5d47e1b 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerParserTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerParserTest.java @@ -1,15 +1,28 @@ package io.swagger.parser; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.util.*; + +import io.swagger.models.*; +import io.swagger.parser.util.DeserializationUtils; +import io.swagger.parser.util.ParseOptions; +import org.apache.commons.io.FileUtils; +import org.testng.Assert; +import org.testng.annotations.Test; + import com.fasterxml.jackson.databind.JsonNode; -import io.swagger.models.ArrayModel; -import io.swagger.models.ComposedModel; -import io.swagger.models.Model; -import io.swagger.models.ModelImpl; -import io.swagger.models.Operation; -import io.swagger.models.Path; -import io.swagger.models.RefModel; -import io.swagger.models.Response; -import io.swagger.models.Swagger; + import io.swagger.models.auth.ApiKeyAuthDefinition; import io.swagger.models.auth.OAuth2Definition; import io.swagger.models.auth.SecuritySchemeDefinition; @@ -23,6 +36,7 @@ import io.swagger.models.parameters.SerializableParameter; import io.swagger.models.properties.ArrayProperty; import io.swagger.models.properties.ByteArrayProperty; +import io.swagger.models.properties.ComposedProperty; import io.swagger.models.properties.IntegerProperty; import io.swagger.models.properties.MapProperty; import io.swagger.models.properties.ObjectProperty; @@ -33,30 +47,111 @@ import io.swagger.parser.util.TestUtils; import io.swagger.util.Json; import io.swagger.util.Yaml; -import org.testng.Assert; -import org.testng.annotations.Test; -import org.testng.reporters.Files; -import java.io.File; -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +public class SwaggerParserTest { -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.testng.Assert.assertNull; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.fail; + @Test + public void testNonArraySchemaWithItems_1636() { + String spec = "swagger: '2.0'\n" + + "info:\n" + + " description: 'This is a TEST.'\n" + + " version: 1.0.0\n" + + " title: Test\n" + + "host: www.abc.com\n" + + "basePath: /api\n" + + "schemes:\n" + + " - http\n" + + "paths:\n" + + " /test:\n" + + " get:\n" + + " summary: Test\n" + + " description: 'test'\n" + + " operationId: test\n" + + " responses:\n" + + " '200':\n" + + " schema:\n" + + " items:\n" + + " $ref: '#/definitions/myResponse'\n" + + " description: myResponse\n" + + "definitions:\n" + + " myResponse:\n" + + " type: object\n" + + " properties:\n" + + " id:\n" + + " type: integer\n" + + " format: int64"; + Swagger swagger = new SwaggerParser().parse(spec); + assertNotNull(swagger); + assertNotNull(swagger.getPaths().get("/test").getGet().getResponses().get("200")); + assertNotNull(swagger.getPaths().get("/test").getGet().getResponses().get("200").getResponseSchema()); + ArrayModel arrayModel = (ArrayModel) swagger.getPaths().get("/test").getGet().getResponses().get("200").getResponseSchema(); + assertNotNull(arrayModel.getItems()); + } -public class SwaggerParserTest { + @Test + public void testArrayRelativeRefs() { + String location = "arrayItemsResolving/Swagger.yaml"; + Swagger swagger = new SwaggerParser().read(location, null, true); + assertNotNull(swagger); + assertTrue(swagger.getDefinitions().size() == 3); + assertNotNull(swagger.getDefinitions().get("InventoryDataItems")); + assertNotNull(swagger.getDefinitions().get("InventoryItem")); + assertNotNull(swagger.getDefinitions().get("Manufacturer")); + } + + @Test + public void testIssueRelativeRefs3() { + String location = "swagger-reference-response.yaml"; + Swagger swagger = new SwaggerParser().read(location, null, true); + assertNotNull(swagger); + } + + @Test + public void testIssue1143() { + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("issue1143.json", null, true); + assertNotNull(result.getSwagger().getDefinitions().get("RedisResource")); + assertNotNull(result.getSwagger().getDefinitions().get("identificacion_usuario_aplicacion")); + } + + @Test + public void testIssue719() { + final Swagger swagger = new SwaggerParser().readWithInfo("extensions-responses/extensions.yaml", null, true).getSwagger(); + Assert.assertNotNull(swagger); + Assert.assertNotNull(swagger.getPaths().get("/something").getGet().getResponsesObject().getVendorExtensions()); + } + + @Test + public void testIssue1204() { + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("issue1204.yaml", null, true); + assertTrue(result.getMessages().size() == 0); + assertNotNull(result.getSwagger()); + } + + @Test + public void testIssue1169() { + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("issue1169.yaml", null, true); + assertTrue(result.getMessages().size() == 0); + assertNotNull(result.getSwagger()); + } + + @Test + public void testIssue1169noSplit() { + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("issue1169-noSplit.yaml", null, true); + assertTrue(result.getMessages().size() == 0); + assertNotNull(result.getSwagger()); + } @Test - public void testIssueRelativeRefs2(){ + public void testIssue1249() { + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("issue-1249.json", null, true); + Assert.assertEquals(result.getMessages().size(), 1); + assertEquals("attribute paths.'user'. For path parameter 'user' the required value should be true", result.getMessages().get(0)); + assertNotNull(result.getSwagger()); + } + + + @Test + public void testIssueRelativeRefs2() { String location = "exampleSpecs/specs/my-domain/test-api/v1/test-api-swagger_v1.json"; Swagger swagger = new SwaggerParser().read(location, null, true); assertNotNull(swagger); @@ -65,7 +160,74 @@ public void testIssueRelativeRefs2(){ ArrayProperty arraySchema = (ArrayProperty) definitions.get("confirmMessageType_v01").getProperties().get("resources"); ObjectProperty prop = (ObjectProperty) arraySchema.getItems(); RefProperty refProperty = (RefProperty) prop.getProperties().get("resourceID"); - assertEquals(refProperty.get$ref(),"#/definitions/simpleIDType_v01"); + assertEquals(refProperty.get$ref(), "#/definitions/simpleIDType_v01"); + } + + @Test + public void testIssue111() { + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("issue-111.yaml", null, true); + assertTrue(result.getMessages().get(0).equals("attribute definitions.Filter.items is missing")); + assertNotNull(result.getSwagger()); + } + + + + @Test + public void testIssue1146() { + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("issue-1146.yaml", null, true); + assertEquals(0, result.getMessages().size()); + } + + @Test + public void testIssueDefinitionWithDots_2() { + Swagger swagger = new SwaggerParser().read("SimpleAPI.yaml"); + assertNotNull(swagger); + } + + @Test + public void testIssueDefinitionWithDots() { + Swagger swagger = new SwaggerParser().read("API-Service-2.0.0-swagger.yaml"); + assertNotNull(swagger); + } + + @Test + public void testIssue995() { + Swagger swagger = new SwaggerParser().read("issue-995/digitalExp-Product-Unresolved.yaml"); + assertNotNull(swagger); + assertTrue(swagger.getDefinitions().size() == 6); + assertNotNull(swagger.getDefinitions().get("MobileProduct")); + assertNotNull(swagger.getDefinitions().get("FixedVoiceProduct")); + assertNotNull(swagger.getDefinitions().get("InternetProduct")); + } + + @Test + public void testIssue927() { + Swagger swagger = new SwaggerParser().read("issue-927/issue-927.yaml"); + assertNotNull(swagger); + assertTrue(swagger.getDefinitions().size() == 3); + assertNotNull(swagger.getDefinitions().get("Pet")); + assertNotNull(swagger.getDefinitions().get("Cat")); + assertNotNull(swagger.getDefinitions().get("Dog")); + } + + @Test + public void testIssue901_2() { + Swagger swagger = new SwaggerParser().read("issue-901/spec2.yaml"); + assertNotNull(swagger); + assertNotNull(swagger.getDefinitions()); + ArrayProperty arraySchema = (ArrayProperty) swagger.getDefinitions().get("Test.Definition").getProperties().get("stuff"); + String internalRef = ((RefProperty) arraySchema.getItems()).get$ref(); + assertEquals(internalRef, "#/definitions/TEST.THING.OUT.Stuff"); + } + + @Test + public void testIssue901() { + Swagger swagger = new SwaggerParser().read("issue-901/spec.yaml"); + assertNotNull(swagger); + String internalRef = ((RefModel) swagger.getPaths().get("/test").getPut().getResponses().get("200").getResponseSchema()).get$ref(); + assertEquals(internalRef, "#/definitions/Test.Definition"); + assertNotNull(swagger.getDefinitions()); + } @Test @@ -81,13 +243,13 @@ public void testIssue845() { SwaggerDeserializationResult swaggerDeserializationResult = new SwaggerParser().readWithInfo(""); assertEquals(swaggerDeserializationResult.getMessages().get(0), "empty or null swagger supplied"); } - + @Test public void testIssue834() { Swagger swagger = new SwaggerParser().read("issue-834/index.yaml", null, true); assertNotNull(swagger); - Response foo200 =swagger.getPaths().get("/foo").getGet().getResponses().get("200"); + Response foo200 = swagger.getPaths().get("/foo").getGet().getResponses().get("200"); assertNotNull(foo200); RefModel model200 = (RefModel) foo200.getResponseSchema(); String foo200SchemaRef = model200.get$ref(); @@ -111,7 +273,7 @@ public void testIssue811_RefSchema_ToRefSchema() { final Swagger swagger = new SwaggerParser().read("oapi-reference-test2/index.yaml", null, true); Assert.assertNotNull(swagger); RefModel model = (RefModel) swagger.getPaths().get("/").getGet().getResponses().get("200").getResponseSchema(); - Assert.assertEquals(model.get$ref() ,"#/definitions/schema-with-reference"); + Assert.assertEquals(model.get$ref(), "#/definitions/schema-with-reference"); } @Test @@ -121,7 +283,7 @@ public void testIssue811() throws Exception { Assert.assertNotNull(swagger); assertTrue(swagger.getPaths().get("/").getGet().getResponses().get("200").getResponseSchema() instanceof RefModel); RefModel model = (RefModel) swagger.getPaths().get("/").getGet().getResponses().get("200").getResponseSchema(); - Assert.assertEquals(model.get$ref(),"#/definitions/schema-with-reference"); + Assert.assertEquals(model.get$ref(), "#/definitions/schema-with-reference"); } @@ -207,10 +369,11 @@ public void testRefPaths() throws Exception { Swagger swagger = parser.parse(yaml); - assertEquals(swagger.getPaths().get("foo"),swagger.getPaths().get("foo2")); - + assertEquals(swagger.getPaths().get("foo"), swagger.getPaths().get("foo2")); + } + @Test public void testModelParameters() throws Exception { String yaml = "swagger: '2.0'\n" + @@ -373,7 +536,6 @@ public void testLoadRelativeFileTree_Json() throws Exception { } - @Test public void testPetstore() throws Exception { SwaggerParser parser = new SwaggerParser(); @@ -433,6 +595,13 @@ public void testLoadRelativeFileTree_Yaml() throws Exception { "src/test/resources/relative-file-references/yaml"); final Swagger swagger = doRelativeFileTest("src/test/resources/relative-file-references/yaml/parent.yaml"); assertNotNull(Yaml.mapper().writeValueAsString(swagger)); + assertTrue(swagger.getParameters().get("param2") instanceof HeaderParameter); + } + + @Test + public void testLoadnestedExternalResponseReferencesFile_Yaml() throws Exception { + final Swagger swagger = doRelativeResponseFileTest("src/test/resources/nested-external-response-references/swagger-root.yaml"); + assertNotNull(Yaml.mapper().writeValueAsString(swagger)); } @Test @@ -562,8 +731,6 @@ public void testIssue() { assertEquals(((Map) swagger.getVendorExtensions().get("x-some-vendor")).get("sometesting"), "bye!"); assertEquals(swagger.getPath("/foo").getVendorExtensions().get("x-something"), "yes, it is supported"); - assertTrue(result.getMessages().size() == 1); - assertEquals(result.getMessages().get(0), "attribute paths.x-nothing is unsupported"); } @Test @@ -624,15 +791,15 @@ public void testIssue292WithCSVCollectionFormat() { QueryParameter qp = (QueryParameter) param; assertEquals(qp.getCollectionFormat(), "csv"); } - + @Test public void testIssue286() { SwaggerParser parser = new SwaggerParser(); Swagger swagger = parser.read("issue_286.yaml"); - Property response = swagger.getPath("/").getGet().getResponses().get("200").getSchema(); - assertTrue(response instanceof RefProperty); - assertEquals(((RefProperty) response).getSimpleRef(), "issue_286_PetList"); + Model response = swagger.getPath("/").getGet().getResponsesObject().get("200").getResponseSchema(); + assertTrue(response instanceof RefModel); + assertEquals(((RefModel) response).getSimpleRef(), "issue_286_PetList"); assertNotNull(swagger.getDefinitions().get("issue_286_Allergy")); } @@ -641,9 +808,9 @@ public void testIssue286WithModel() { SwaggerParser parser = new SwaggerParser(); Swagger swagger = parser.read("issue_286.yaml"); - Model response = swagger.getPath("/").getGet().getResponses().get("200").getResponseSchema(); + Model response = swagger.getPath("/").getGet().getResponsesObject().get("200").getResponseSchema(); assertTrue(response instanceof RefModel); - assertEquals( "issue_286_PetList", ((RefModel) response).getSimpleRef()); + assertEquals("issue_286_PetList", ((RefModel) response).getSimpleRef()); assertNotNull(swagger.getDefinitions().get("issue_286_Allergy")); } @@ -770,6 +937,46 @@ private Swagger doRelativeFileTest(String location) { return swagger; } + private Swagger doRelativeResponseFileTest(String location) { + SwaggerParser parser = new SwaggerParser(); + SwaggerDeserializationResult readResult = parser.readWithInfo(location, null, true); + + if (readResult.getMessages().size() > 0) { + Json.prettyPrint(readResult.getMessages()); + } + final Swagger swagger = readResult.getSwagger(); + + Json.prettyPrint(swagger); + + final Path path = swagger.getPath("/users"); + assertEquals(path.getClass(), Path.class); //we successfully converted the RefPath to a Path + + final Operation operation = path.getGet(); + + final Map responsesMap = operation.getResponses(); + + assertResponse(swagger, responsesMap, "200", "OK", "#/definitions/User"); + + final Map definitions = swagger.getDefinitions(); + final ModelImpl refInDefinitions = (ModelImpl) definitions.get("UserX"); + expectedPropertiesInModel(refInDefinitions, "address"); + + final ModelImpl refInDefinitionsAddress = (ModelImpl) definitions.get("Address"); + expectedPropertiesInModel(refInDefinitionsAddress, "postal", "country"); + + final ModelImpl refInDefinitionsCountry = (ModelImpl) definitions.get("Country"); + expectedPropertiesInModel(refInDefinitionsCountry, "name"); + + final ModelImpl refInDefinitionsAddress_2 = (ModelImpl) definitions.get("Address_2"); + expectedPropertiesInModel(refInDefinitionsAddress_2, "postal", "country"); + + final ModelImpl refInDefinitionsCountry_2 = (ModelImpl) definitions.get("Country_2"); + expectedPropertiesInModel(refInDefinitionsCountry_2, "name"); + + return swagger; + } + + private void expectedPropertiesInModel(ModelImpl model, String... expectedProperties) { assertEquals(model.getProperties().size(), expectedProperties.length); for (String expectedProperty : expectedProperties) { @@ -780,9 +987,9 @@ private void expectedPropertiesInModel(ModelImpl model, String... expectedProper private void assertResponse(Swagger swagger, Map responsesMap, String responseCode, String expectedDescription, String expectedSchemaRef) { final Response response = responsesMap.get(responseCode); - final RefProperty schema = (RefProperty) response.getSchema(); + final RefModel schema = (RefModel) response.getResponseSchema(); assertEquals(response.getDescription(), expectedDescription); - assertEquals(schema.getClass(), RefProperty.class); + assertEquals(schema.getClass(), RefModel.class); assertEquals(schema.get$ref(), expectedSchemaRef); assertTrue(swagger.getDefinitions().containsKey(schema.getSimpleRef())); } @@ -804,7 +1011,7 @@ public void testNestedReferences() { assertTrue(swagger.getDefinitions().containsKey("externalObject")); assertTrue(swagger.getDefinitions().containsKey("referencedByLocalElement")); assertTrue(swagger.getDefinitions().containsKey("referencedBy")); - assertEquals(((RefProperty)swagger.getDefinitions().get("externalObject").getProperties().get("hello1")).get$ref(), + assertEquals(((RefProperty) swagger.getDefinitions().get("externalObject").getProperties().get("hello1")).get$ref(), "#/definitions/referencedByLocalElement"); //issue #434 } @@ -911,18 +1118,18 @@ public void testConverterIssue17() throws Exception { " collectionFormat: csv\n" + " responses:\n" + " 200:\n" + - " description: Successful response\n"+ + " description: Successful response\n" + " schema:\n" + " $ref: '#/definitions/Content'\n" + "definitions:\n" + - " Content:\n" + - " type: object"; + " Content:\n" + + " type: object"; SwaggerDeserializationResult result = new SwaggerParser().readWithInfo(yaml, Boolean.FALSE); assertNotNull(result.getSwagger()); - assertEquals(((RefProperty) result.getSwagger().getPaths(). - get("/persons").getGet().getResponses().get("200") - .getSchema()).get$ref(), "#/definitions/Content"); + assertEquals(((RefModel) result.getSwagger().getPaths(). + get("/persons").getGet().getResponsesObject().get("200") + .getResponseSchema()).get$ref(), "#/definitions/Content"); } @Test @@ -956,6 +1163,7 @@ public void testIssue393() { assertNotNull(swagger.getVendorExtensions().get("x-error-defs")); } + @Test public void testBadFormat() throws Exception { SwaggerParser parser = new SwaggerParser(); final Swagger swagger = parser.read("src/test/resources/bad_format.yaml"); @@ -995,6 +1203,58 @@ public void testBadFormat() throws Exception { assertEquals(queryParameter.isUniqueItems(), true); } + @Test + public void testNumberAttributes() throws Exception { + SwaggerParser parser = new SwaggerParser(); + Swagger swagger = parser.read(TestUtils.getResourceAbsolutePath("/number_attributes.yaml")); + + ModelImpl numberType = (ModelImpl) swagger.getDefinitions().get("NumberType"); + assertNotNull(numberType); + assertNotNull(numberType.getEnum()); + assertEquals(numberType.getEnum().size(), 2); + List numberTypeEnumValues = numberType.getEnum(); + assertEquals(numberTypeEnumValues.get(0), "1.0"); + assertEquals(numberTypeEnumValues.get(1), "2.0"); + assertEquals(numberType.getDefaultValue(), new BigDecimal("1.0")); + assertEquals(numberType.getMinimum(), new BigDecimal("1.0")); + assertEquals(numberType.getMaximum(), new BigDecimal("2.0")); + + ModelImpl numberDoubleType = (ModelImpl) swagger.getDefinitions().get("NumberDoubleType"); + assertNotNull(numberDoubleType); + assertNotNull(numberDoubleType.getEnum()); + assertEquals(numberDoubleType.getEnum().size(), 2); + List numberDoubleTypeEnumValues = numberDoubleType.getEnum(); + assertEquals(numberDoubleTypeEnumValues.get(0), "1.0"); + assertEquals(numberDoubleTypeEnumValues.get(1), "2.0"); + assertEquals(numberDoubleType.getDefaultValue(), new BigDecimal("1.0")); + assertEquals(numberDoubleType.getMinimum(), new BigDecimal("1.0")); + assertEquals(numberDoubleType.getMaximum(), new BigDecimal("2.0")); + + ModelImpl integerType = (ModelImpl) swagger.getDefinitions().get("IntegerType"); + assertNotNull(integerType); + assertNotNull(integerType.getEnum()); + assertEquals(integerType.getEnum().size(), 2); + List integerTypeEnumValues = integerType.getEnum(); + assertEquals(integerTypeEnumValues.get(0), "1"); + assertEquals(integerTypeEnumValues.get(1), "2"); + assertEquals(integerType.getDefaultValue(), new Integer("1")); + assertEquals(integerType.getMinimum(), new BigDecimal("1")); + assertEquals(integerType.getMaximum(), new BigDecimal("2")); + + ModelImpl integerInt32Type = (ModelImpl) swagger.getDefinitions().get("IntegerInt32Type"); + assertNotNull(integerInt32Type); + assertNotNull(integerInt32Type.getEnum()); + assertEquals(integerInt32Type.getEnum().size(), 2); + List integerInt32TypeEnumValues = integerInt32Type.getEnum(); + assertEquals(integerInt32TypeEnumValues.get(0), "1"); + assertEquals(integerInt32TypeEnumValues.get(1), "2"); + assertEquals(integerInt32Type.getDefaultValue(), new Integer("1")); + assertEquals(integerInt32Type.getMinimum(), new BigDecimal("1")); + assertEquals(integerInt32Type.getMaximum(), new BigDecimal("2")); + + + } + @Test public void testDefinitionExample() throws Exception { SwaggerParser parser = new SwaggerParser(); @@ -1003,46 +1263,111 @@ public void testDefinitionExample() throws Exception { ModelImpl model; ArrayModel arrayModel; - model = (ModelImpl)swagger.getDefinitions().get("NumberType"); - assertEquals((Double)model.getExample(), 2.0d, 0d); + model = (ModelImpl) swagger.getDefinitions().get("NumberType"); + assertEquals((Double) model.getExample(), 2.0d, 0d); - model = (ModelImpl)swagger.getDefinitions().get("IntegerType"); - assertEquals((int)model.getExample(), 2); + model = (ModelImpl) swagger.getDefinitions().get("IntegerType"); + assertEquals((int) model.getExample(), 2); - model = (ModelImpl)swagger.getDefinitions().get("StringType"); - assertEquals((String)model.getExample(), "2"); + model = (ModelImpl) swagger.getDefinitions().get("StringType"); + assertEquals((String) model.getExample(), "2"); - model = (ModelImpl)swagger.getDefinitions().get("ObjectType"); + model = (ModelImpl) swagger.getDefinitions().get("ObjectType"); assertTrue(model.getExample() instanceof Map); Map objectExample = (Map) model.getExample(); - assertEquals((String)objectExample.get("propertyA"), "valueA"); - assertEquals((Integer)objectExample.get("propertyB"), new Integer(123)); + assertEquals((String) objectExample.get("propertyA"), "valueA"); + assertEquals((Integer) objectExample.get("propertyB"), new Integer(123)); - arrayModel = (ArrayModel)swagger.getDefinitions().get("ArrayType"); + arrayModel = (ArrayModel) swagger.getDefinitions().get("ArrayType"); assertTrue(arrayModel.getExample() instanceof List); List arrayExample = (List) arrayModel.getExample(); - assertEquals((String)arrayExample.get(0).get("propertyA"), "valueA1"); - assertEquals((Integer)arrayExample.get(0).get("propertyB"), new Integer(123)); - assertEquals((String)arrayExample.get(1).get("propertyA"), "valueA2"); - assertEquals((Integer)arrayExample.get(1).get("propertyB"), new Integer(456)); + assertEquals((String) arrayExample.get(0).get("propertyA"), "valueA1"); + assertEquals((Integer) arrayExample.get(0).get("propertyB"), new Integer(123)); + assertEquals((String) arrayExample.get(1).get("propertyA"), "valueA2"); + assertEquals((Integer) arrayExample.get(1).get("propertyB"), new Integer(456)); - model = (ModelImpl)swagger.getDefinitions().get("NumberTypeStringExample"); - assertEquals((String)model.getExample(), "2.0"); + model = (ModelImpl) swagger.getDefinitions().get("NumberTypeStringExample"); + assertEquals((String) model.getExample(), "2.0"); - model = (ModelImpl)swagger.getDefinitions().get("IntegerTypeStringExample"); - assertEquals((String)model.getExample(), "2"); + model = (ModelImpl) swagger.getDefinitions().get("IntegerTypeStringExample"); + assertEquals((String) model.getExample(), "2"); - model = (ModelImpl)swagger.getDefinitions().get("StringTypeStringExample"); - assertEquals((String)model.getExample(), "2"); + model = (ModelImpl) swagger.getDefinitions().get("StringTypeStringExample"); + assertEquals((String) model.getExample(), "2"); - model = (ModelImpl)swagger.getDefinitions().get("ObjectTypeStringExample"); - assertEquals((String)model.getExample(), "{\"propertyA\": \"valueA\", \"propertyB\": 123}"); + model = (ModelImpl) swagger.getDefinitions().get("ObjectTypeStringExample"); + assertEquals((String) model.getExample(), "{\"propertyA\": \"valueA\", \"propertyB\": 123}"); arrayModel = (ArrayModel) swagger.getDefinitions().get("ArrayTypeStringExample"); - assertEquals((String)arrayModel.getExample(), "[{\"propertyA\": \"valueA1\", \"propertyB\": 123}, {\"propertyA\": \"valueA2\", \"propertyB\": 456}]"); - } - - @Test + assertEquals((String) arrayModel.getExample(), "[{\"propertyA\": \"valueA1\", \"propertyB\": 123}, {\"propertyA\": \"valueA2\", \"propertyB\": 456}]"); + } + + @Test + public void testExternalParametersRealExample() { + SwaggerParser parser = new SwaggerParser(); + final Swagger swagger = parser.read( + "src/test/resources/parameters-external/data-plane/ComputerVision/stable/v1.0/ComputerVision.json"); + assertNotNull(swagger); + final Path path = swagger.getPath("/analyze"); + assertNotNull(path); + final Operation operation = path.getPost(); + assertNotNull(operation); + final List parameters = operation.getParameters(); + assertNotNull(parameters); + final BodyParameter parameter = (BodyParameter) parameters.get(3); + assertNotNull(parameter); + assertEquals(parameter.getName(), "ImageUrl"); + final RefModel schema = (RefModel) parameter.getSchema(); + assertNotNull(schema); + final String simpleRef = schema.getSimpleRef(); + assertNotNull(simpleRef); + final String message = "Where is " + simpleRef + "?"; + if (schema.getReference().startsWith("#/definitions/")) { + assertNotNull(message, swagger.getDefinitions().get(simpleRef)); + } else if (schema.getReference().startsWith("#/parameters/")) { + assertNotNull(message, swagger.getParameters().get(simpleRef)); + } else { + Assert.fail(message); + } + } + + @Test + public void testExternalParametersSimpleExample() { + SwaggerParser parser = new SwaggerParser(); + final Swagger swagger = parser.read( + "src/test/resources/parameters-external/simple/externals-level-0.json"); + checkExternalParameters(swagger, 1, ModelImpl.class, null); + checkExternalParameters(swagger, 2, RefModel.class, "#/definitions/D-Level1Thing3"); + checkExternalParameters(swagger, 3, RefModel.class, "#/definitions/P-Level2Thing3"); + } + + private void checkExternalParameters(Swagger swagger, int id, Class expectedClass, String expectedRef) { + assertNotNull(swagger); + final String pathKey = "/path-" + id; + final Path path = swagger.getPath(pathKey); + assertNotNull(pathKey, path); + final Operation operation = path.getGet(); + assertNotNull(operation); + final List parameters = operation.getParameters(); + assertNotNull(parameters); + final BodyParameter bodyParameter = (BodyParameter) parameters.get(0); + assertNotNull(bodyParameter); + final String expectedName = "Level1Thing" + id; + assertEquals(expectedName, bodyParameter.getName()); + final Model schema = bodyParameter.getSchema(); + assertNotNull(schema); + if (expectedClass == ModelImpl.class) { + assertEquals("string", ((ModelImpl) schema).getType()); + } else if (expectedClass == RefModel.class) { + final RefModel refSchema = (RefModel) schema; + final String ref = refSchema.get$ref(); + assertEquals(expectedRef, ref); + final Model model = swagger.getDefinitions().get(refSchema.getSimpleRef()); + assertNotNull(model); + } + } + + @Test public void testIssue357() { SwaggerParser parser = new SwaggerParser(); final Swagger swagger = parser.read("src/test/resources/issue_357.yaml"); @@ -1150,8 +1475,8 @@ public void testIssue594() { " description: 'OK'\n"; SwaggerDeserializationResult result = new SwaggerParser().readWithInfo(yaml); assertNotNull(result.getSwagger()); - ArrayModel schema = (ArrayModel)((BodyParameter)result.getSwagger().getPaths().get("/test").getPost().getParameters().get(0)).getSchema(); - assertEquals(((RefProperty)schema.getItems()).get$ref(),"#/definitions/Pet"); + ArrayModel schema = (ArrayModel) ((BodyParameter) result.getSwagger().getPaths().get("/test").getPost().getParameters().get(0)).getSchema(); + assertEquals(((RefProperty) schema.getItems()).get$ref(), "#/definitions/Pet"); assertNotNull(schema.getMaxItems()); assertNotNull(schema.getMinItems()); @@ -1236,6 +1561,21 @@ public void checkAllOfWithRelativeReferencesIssue604() { assertEquals(2, swagger.getDefinitions().size()); } + @Test(description = "Test that validate resolution of external references in allOf of property") + public void checkExtRefResolveInPropertiesWithAllOf() { + Swagger swagger = new SwaggerParser().read("src/test/resources/allOf-property-relative-file-references/parent.yaml"); + assertEquals(2, swagger.getDefinitions().size()); + assertEquals(1, swagger.getDefinitions().get("test").getProperties().size()); + + ComposedProperty property = (ComposedProperty) swagger.getDefinitions().get("test").getProperties().get("property"); + assertEquals(1, property.getVendorExtensions().size()); + assertEquals(1, property.getAllOf().size()); + + RefProperty refProperty = (RefProperty) property.getAllOf().get(0); + assertEquals("#/definitions/def.def", refProperty.get$ref()); + + } + @Test(description = "A string example should not be over quoted when parsing a yaml string") public void readingSpecStringShouldNotOverQuotingStringExample() throws Exception { SwaggerParser parser = new SwaggerParser(); @@ -1247,10 +1587,10 @@ public void readingSpecStringShouldNotOverQuotingStringExample() throws Exceptio @Test(description = "A string example should not be over quoted when parsing a yaml node") public void readingSpecNodeShouldNotOverQuotingStringExample() throws Exception { - String yaml = Files.readFile(new File("src/test/resources/over-quoted-example.yaml")); + String yaml = new String(Files.readAllBytes(new File("src/test/resources/over-quoted-example.yaml").toPath()), "UTF-8"); JsonNode rootNode = Yaml.mapper().readValue(yaml, JsonNode.class); SwaggerParser parser = new SwaggerParser(); - Swagger swagger = parser.read(rootNode,true); + Swagger swagger = parser.read(rootNode, true); Map definitions = swagger.getDefinitions(); assertEquals("NoQuotePlease", definitions.get("CustomerType").getExample()); @@ -1262,9 +1602,9 @@ public void testRefNameConflicts() throws Exception { assertTrue(swagger.getDefinitions().size() == 2); - assertEquals("#/definitions/PersonObj", ((RefProperty) swagger.getPath("/newPerson").getPost().getResponses().get("200").getSchema()).get$ref()); - assertEquals("#/definitions/PersonObj_2", ((RefProperty) swagger.getPath("/oldPerson").getPost().getResponses().get("200").getSchema()).get$ref()); - assertEquals("#/definitions/PersonObj_2", ((RefProperty) swagger.getPath("/yetAnotherPerson").getPost().getResponses().get("200").getSchema()).get$ref()); + assertEquals("#/definitions/PersonObj", ((RefModel) swagger.getPath("/newPerson").getPost().getResponsesObject().get("200").getResponseSchema()).get$ref()); + assertEquals("#/definitions/PersonObj_2", ((RefModel) swagger.getPath("/oldPerson").getPost().getResponsesObject().get("200").getResponseSchema()).get$ref()); + assertEquals("#/definitions/PersonObj_2", ((RefModel) swagger.getPath("/yetAnotherPerson").getPost().getResponsesObject().get("200").getResponseSchema()).get$ref()); assertEquals("local", swagger.getDefinitions().get("PersonObj").getProperties().get("location").getExample()); assertEquals("referred", swagger.getDefinitions().get("PersonObj_2").getProperties().get("location").getExample()); } @@ -1276,7 +1616,7 @@ public void testRefAdditionalProperties() throws Exception { Assert.assertNotNull(swagger); Assert.assertTrue(swagger.getDefinitions().size() == 3); - + Assert.assertNotNull(swagger.getDefinitions().get("link-object")); Assert.assertNotNull(swagger.getDefinitions().get("rel-data")); Assert.assertNotNull(swagger.getDefinitions().get("result")); @@ -1344,4 +1684,204 @@ public void testIssue844() { assertEquals(swagger.getSwagger().getPath("/pets/{id}").getGet().getParameters().get(0).getIn(), "header"); } -} \ No newline at end of file + @Test + public void testIssue258() { + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("duplicateOperationId.json", null, true); + assertNotNull(result); + assertNotNull(result.getSwagger()); + assertEquals(result.getMessages().get(0), "attribute paths.'/pets/{id}'(post).operationId is repeated"); + } + + @Test + public void testIssue913() { + SwaggerParser parser = new SwaggerParser(); + final Swagger swagger = parser.read("src/test/resources/issue-913/BS/ApiSpecification.yaml"); + Assert.assertNotNull(swagger); + Assert.assertNotNull(swagger.getDefinitions().get("indicatorType")); + Assert.assertEquals(swagger.getDefinitions().get("indicatorType").getProperties().size(), 1); + } + + @Test + public void testIssue432() { + + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("src/test/resources/issue-1432.yaml", null, true); + assertNotNull(result); + assertEquals("attribute paths.'/tickets'(get).responses.200.title is unexpected",result.getMessages().get(0)); + } + + @Test + public void testIssue1541() { + SwaggerParser parser = new SwaggerParser(); + final Swagger swagger = parser.read("src/test/resources/issue-1541/main.yaml"); + assertNotNull(swagger); + + Model inlineSchema = swagger.getPaths() + .get("/inline_response") + .getGet() + .getResponses() + .get("200") + .getResponseSchema(); + + assertNotNull(inlineSchema); + assertNotNull(inlineSchema.getProperties()); + assertNotNull(inlineSchema.getProperties().get("a")); + assertEquals("number", inlineSchema.getProperties().get("a").getType()); + + Model responseRef = swagger.getPaths() + .get("/ref_response") + .getGet() + .getResponses() + .get("200") + .getResponseSchema(); + + assertEquals("#/definitions/ref_response_object", responseRef.getReference()); + + Model refSchema = swagger.getDefinitions() + .get("ref_response_object"); + + assertEquals("number", refSchema.getProperties().get("a").getType()); + } + + @Test + public void testRequiredItemsInComposedModel() { + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("src/test/resources/allOf-example/allOf.yaml", null, true); + assertNotNull(result); + final Swagger swagger = result.getSwagger(); + final Model model = swagger.getDefinitions().get("UserRegister"); + final ComposedModel composedModel = (ComposedModel) model; + + assertNotNull(composedModel.getRequired()); + assertFalse(composedModel.getRequired().isEmpty()); + + assertEquals("password", composedModel.getRequired().get(0)); + } + + @Test + public void testInlineModelResolver() throws IOException { + String inputSpec = FileUtils.readFileToString(new File("src/test/resources/flatten.json")); + JsonNode rootNode = DeserializationUtils.deserializeIntoTree(inputSpec, ""); + ParseOptions parseOptions = new ParseOptions(); + parseOptions.setFlatten(true); + + Swagger swagger = new SwaggerParser().read(rootNode, new ArrayList<>(), parseOptions); + + assertNotNull(swagger); + Map definitions = swagger.getDefinitions(); + assertTrue(definitions.containsKey("User")); + assertTrue(definitions.containsKey("User_address")); + + Model userAddress = definitions.get("User_address"); + assertTrue(userAddress.getProperties().containsKey("city")); + assertTrue(userAddress.getProperties().containsKey("street")); + } + + @Test + public void testInlineModelResolverByLocation() { + ParseOptions parseOptions = new ParseOptions(); + parseOptions.setFlatten(true); + + Swagger swagger = new SwaggerParser().read("src/test/resources/flatten.json", new ArrayList<>(), parseOptions); + + assertNotNull(swagger); + Map definitions = swagger.getDefinitions(); + assertTrue(definitions.containsKey("User")); + assertTrue(definitions.containsKey("User_address")); + + Model userAddress = definitions.get("User_address"); + assertTrue(userAddress.getProperties().containsKey("city")); + assertTrue(userAddress.getProperties().containsKey("street")); + } + + @Test(description = "Test safe resolving") + public void test20SafeURLResolving() throws IOException { + String yaml = new String(Files.readAllBytes(new File("src/test/resources/safelyResolve/oas20SafeUrlResolvingWithPetstore.yaml").toPath()), "UTF-8"); + JsonNode jsonNodeSwagger = Yaml.mapper().readValue(yaml, JsonNode.class); + + ParseOptions parseOptions = new ParseOptions(); + parseOptions.setResolve(true); + parseOptions.setSafelyResolveURL(true); + List allowList = Collections.emptyList(); + List blockList = Collections.emptyList(); + parseOptions.setRemoteRefAllowList(allowList); + parseOptions.setRemoteRefBlockList(blockList); + + new SwaggerParser().read(jsonNodeSwagger, null, parseOptions); + } + + @Test(description = "Test safe resolving with blocked URL") + public void test20SafeURLResolvingWithBlockedURL() throws IOException { + String yaml = new String(Files.readAllBytes(new File("src/test/resources/safelyResolve/oas20SafeUrlResolvingWithPetstore.yaml").toPath()), "UTF-8"); + JsonNode jsonNodeSwagger = Yaml.mapper().readValue(yaml, JsonNode.class); + + ParseOptions parseOptions = new ParseOptions(); + parseOptions.setResolve(true); + parseOptions.setSafelyResolveURL(true); + List allowList = Collections.emptyList(); + List blockList = Arrays.asList("petstore3.swagger.io"); + parseOptions.setRemoteRefAllowList(allowList); + parseOptions.setRemoteRefBlockList(blockList); + + assertThrows(RuntimeException.class, () -> { + new SwaggerParser().read(jsonNodeSwagger, null, parseOptions); + }); + } + + @Test(description = "Test safe resolving with turned off safelyResolveURL option") + public void test20SafeURLResolvingWithTurnedOffSafeResolving() throws IOException { + String yaml = new String(Files.readAllBytes(new File("src/test/resources/safelyResolve/oas20SafeUrlResolvingWithPetstore.yaml").toPath()), "UTF-8"); + JsonNode jsonNodeSwagger = Yaml.mapper().readValue(yaml, JsonNode.class); + + ParseOptions parseOptions = new ParseOptions(); + parseOptions.setResolve(false); + parseOptions.setSafelyResolveURL(true); + List allowList = Collections.emptyList(); + List blockList = Arrays.asList("petstore3.swagger.io"); + parseOptions.setRemoteRefAllowList(allowList); + parseOptions.setRemoteRefBlockList(blockList); + + new SwaggerParser().read(jsonNodeSwagger, null, parseOptions); + } + + @Test(description = "Test safe resolving with localhost and blocked url") + public void test20SafeURLResolvingWithLocalhostAndBlockedURL() throws IOException { + String yaml = new String(Files.readAllBytes(new File("src/test/resources/safelyResolve/oas20SafeUrlResolvingWithLocalhost.yaml").toPath()), "UTF-8"); + JsonNode jsonNodeSwagger = Yaml.mapper().readValue(yaml, JsonNode.class); + + ParseOptions parseOptions = new ParseOptions(); + parseOptions.setResolve(true); + parseOptions.setSafelyResolveURL(true); + List allowList = Collections.emptyList(); + List blockList = Arrays.asList("petstore.swagger.io"); + parseOptions.setRemoteRefAllowList(allowList); + parseOptions.setRemoteRefBlockList(blockList); + + assertThrows(RuntimeException.class, () -> { + new SwaggerParser().read(jsonNodeSwagger, null, parseOptions); + }); } + + @Test(description = "Test safe resolving with localhost url") + public void test20SafeURLResolvingWithLocalhost() throws IOException { + String yaml = new String(Files.readAllBytes(new File("src/test/resources/safelyResolve/oas20SafeUrlResolvingWithLocalhost.yaml").toPath()), "UTF-8"); + JsonNode jsonNodeSwagger = Yaml.mapper().readValue(yaml, JsonNode.class); + + ParseOptions parseOptions = new ParseOptions(); + parseOptions.setResolve(true); + parseOptions.setSafelyResolveURL(true); + List allowList = Collections.emptyList(); + List blockList = Collections.emptyList(); + parseOptions.setRemoteRefAllowList(allowList); + parseOptions.setRemoteRefBlockList(blockList); + + assertThrows(RuntimeException.class, () -> { + new SwaggerParser().read(jsonNodeSwagger, null, parseOptions); + }); + } + + @Test + public void testIssueSwg14378() { + + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("src/test/resources/issue-swg-14378.yaml", null, false); + assertNotNull(result); + assertEquals("attribute info.version is missing",result.getMessages().get(0)); + } +} diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerReaderTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerReaderTest.java index f3af580c0b..e5dd978186 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerReaderTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerReaderTest.java @@ -1,6 +1,5 @@ package io.swagger.parser; -import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.matchers.SerializationMatchers; import io.swagger.models.*; import io.swagger.models.parameters.Parameter; @@ -252,6 +251,7 @@ public void testIssue205() { "swagger: '2.0'\n" + "info:\n" + " title: nice\n" + + " version: '1'\n" + "paths: {}\n" + "definitions:\n" + " Empty:\n" + @@ -267,7 +267,6 @@ public void testIssue205() { assertTrue(definition instanceof ModelImpl); } - @Test public void testIssue136() { String spec = @@ -282,7 +281,7 @@ public void testIssue136() { " 200:\n" + " description: 'the pet'\n" + " schema:\n" + - " $ref: 'http://petstore.swagger.io/v2/swagger.json#/definitions/Pet'"; + " $ref: 'https://petstore.swagger.io/v2/swagger.json#/definitions/Pet'"; SwaggerDeserializationResult result = new SwaggerParser().readWithInfo(spec); diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerResolverTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerResolverTest.java index b6c6396431..7895f637e7 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerResolverTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/SwaggerResolverTest.java @@ -210,7 +210,7 @@ private void testResponseRemoteRefs(String remoteRef) { .responseSchema(new RefModel(remoteRef))))); final Swagger resolved = new SwaggerResolver(swagger, null).resolve(); - final Response response = swagger.getPaths().get("/fun").getGet().getResponses().get("200"); + final Response response = swagger.getPaths().get("/fun").getGet().getResponsesObject().get("200"); final RefModel ref = (RefModel) response.getResponseSchema(); assertEquals(ref.get$ref(), "#/definitions/Tag"); assertNotNull(swagger.getDefinitions().get("Tag")); @@ -237,7 +237,7 @@ public void testYamlArrayResponseRemoteRefs() { new RefProperty(REMOTE_REF_YAML)))))); final Swagger resolved = new SwaggerResolver(swagger, null).resolve(); - final Response response = swagger.getPaths().get("/fun").getGet().getResponses().get("200"); + final Response response = swagger.getPaths().get("/fun").getGet().getResponsesObject().get("200"); final ArrayModel array = (ArrayModel) response.getResponseSchema(); assertNotNull(array.getItems()); @@ -290,7 +290,7 @@ public void testSharedResponses() { swagger.response("foo", new Response().description("ok!")); final Swagger resolved = new SwaggerResolver(swagger, null).resolve(); - Response response = resolved.getPath("/fun").getGet().getResponses().get("200"); + Response response = resolved.getPath("/fun").getGet().getResponsesObject().get("200"); assertTrue(response.getDescription().equals("ok!")); assertTrue(response instanceof Response); } diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/processors/ExternalRefProcessorTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/processors/ExternalRefProcessorTest.java index 5c2c718023..6ac1e6ddf0 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/processors/ExternalRefProcessorTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/processors/ExternalRefProcessorTest.java @@ -1,7 +1,9 @@ package io.swagger.parser.processors; +import io.swagger.models.ComposedModel; import io.swagger.models.Model; import io.swagger.models.ModelImpl; +import io.swagger.models.RefModel; import io.swagger.models.Swagger; import io.swagger.models.properties.Property; import io.swagger.models.properties.RefProperty; @@ -11,12 +13,16 @@ import mockit.Expectations; import mockit.Injectable; import mockit.StrictExpectations; + import org.testng.annotations.Test; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertSame; import static org.testng.AssertJUnit.assertTrue; @@ -129,4 +135,46 @@ public void testNestedExternalRefs(@Injectable final Model mockedModel){ assertTrue(testedSwagger.getDefinitions().get("Contact")!=null); assertTrue(testedSwagger.getDefinitions().get("Address")!=null); } + + @Test + public void testEmptyModelWithDiscriminator(@Injectable final Model mockedModel){ + //Swagger test instance + Swagger testedSwagger = new Swagger(); + + final String contactURL = "#/definitions/Contact"; + final String emailContactURL = "#/definitions/EmailContact"; + + //Start with EmailContact model inherited from Contact model + final ComposedModel emailContactModel = new ComposedModel(); + RefModel contactRefModel = new RefModel(contactURL); + List refs = new ArrayList<>(); + refs.add(contactRefModel); + emailContactModel.setAllOf(refs); + Property emailProp = new StringProperty(); + emailProp.setName("Email"); + emailProp.setRequired(true); + Map emailContactProps = new HashMap(); + emailContactProps.put("Email", emailProp); + emailContactModel.setProperties(emailContactProps); + + //create Contact, an empty model with discriminator + final ModelImpl contactModel = new ModelImpl(); + contactModel.setDiscriminator("type"); + + new Expectations(){{ + cache.loadRef(emailContactURL, RefFormat.INTERNAL, Model.class); + result = emailContactModel; + times = 1; + + cache.loadRef(contactURL, RefFormat.RELATIVE, Model.class); + result = contactModel; + times = 1; + }}; + + String actualRef = new ExternalRefProcessor(cache, testedSwagger).processRefToExternalDefinition(emailContactURL, RefFormat.INTERNAL); + assertEquals(actualRef, "EmailContact"); + + assertSame(testedSwagger.getDefinitions().get("EmailContact"), emailContactModel); + assertSame(testedSwagger.getDefinitions().get("Contact"), contactModel); + } } diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/processors/ModelProcessorTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/processors/ModelProcessorTest.java index 587dd4a4a2..0778c5c031 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/processors/ModelProcessorTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/processors/ModelProcessorTest.java @@ -2,6 +2,7 @@ import io.swagger.models.*; import io.swagger.models.properties.Property; +import io.swagger.models.properties.RefProperty; import io.swagger.models.refs.RefFormat; import io.swagger.parser.ResolverCache; import mockit.*; @@ -150,4 +151,56 @@ private void setupPropertyAndExternalRefProcessors() { result = externalRefProcessor; }}; } + + + @Test + public void testProcessComposedModelWithProperties(@Injectable final Property property1) throws Exception { + setupPropertyAndExternalRefProcessors(); + + final String ref1 = "http://my.company.com/path/to/file.json#/foo/bar"; + final String ref2 = "http://my.company.com/path/to/file.json#/this/that"; + final String ref3 = "http://my.company.com/path/to/file.json#/hello/world"; + final String ref4 = "http://my.company.com/path/to/file.json#/hello/ref"; + final Property property2 = new RefProperty(ref4); + + ModelProcessor modelProcessor = new ModelProcessor(cache, swagger); + + new Expectations() {{ + externalRefProcessor.processRefToExternalDefinition(ref1, RefFormat.URL); + times = 1; + result = "bar"; + externalRefProcessor.processRefToExternalDefinition(ref2, RefFormat.URL); + times = 1; + result = "that"; + externalRefProcessor.processRefToExternalDefinition(ref3, RefFormat.URL); + times = 1; + result = "world"; + propertyProcessor.processProperty(property1); + times = 1; + propertyProcessor.processProperty(property2); + times = 1; + }}; + + ComposedModel composedModel = new ComposedModel(); + composedModel.child(new RefModel(ref1)); + composedModel.parent(new RefModel(ref2)); + composedModel.interfaces(Arrays.asList(new RefModel(ref3))); + composedModel.addProperty("foo", property1); + composedModel.addProperty("bar", property2); + + modelProcessor.processModel(composedModel); + + new FullVerifications() {{ + externalRefProcessor.processRefToExternalDefinition(ref1, RefFormat.URL); + times = 1; + externalRefProcessor.processRefToExternalDefinition(ref2, RefFormat.URL); + times = 1; + externalRefProcessor.processRefToExternalDefinition(ref3, RefFormat.URL); + times = 1; + }}; + + assertEquals(((RefModel) composedModel.getChild()).get$ref(), "#/definitions/bar"); + assertEquals(((RefModel) composedModel.getParent()).get$ref(), "#/definitions/that"); + assertEquals((composedModel.getInterfaces().get(0)).get$ref(), "#/definitions/world"); + } } diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/processors/OperationProcessorTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/processors/OperationProcessorTest.java index f7259ca041..db606962c0 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/processors/OperationProcessorTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/processors/OperationProcessorTest.java @@ -76,7 +76,7 @@ public void testProcessOperation(@Injectable final List inputParamete new FullVerifications() {{}}; - assertEquals(operation.getResponses().get("200"), resolvedResponse); + assertEquals(operation.getResponsesObject().get("200"), resolvedResponse); assertEquals(operation.getParameters(), outputParameterList); } } diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/util/InlineModelResolverTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/util/InlineModelResolverTest.java new file mode 100644 index 0000000000..d02bd1cc2b --- /dev/null +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/util/InlineModelResolverTest.java @@ -0,0 +1,1021 @@ +package io.swagger.parser.util; + +import io.swagger.models.*; +import io.swagger.models.parameters.BodyParameter; +import io.swagger.models.parameters.Parameter; +import io.swagger.models.properties.*; +import io.swagger.util.Json; +import org.apache.commons.lang3.StringUtils; +import org.testng.annotations.Test; + +import java.util.Map; + +import static org.testng.AssertJUnit.*; + +@SuppressWarnings("static-method") +public class InlineModelResolverTest { + @Test + public void resolveInlineModelTestWithoutTitle() throws Exception { + Swagger swagger = new Swagger(); + + swagger.addDefinition("User", new ModelImpl() + .name("user") + .description("a common user") + .property("name", new StringProperty()) + .property("address", new ObjectProperty() + ._default("default") + .access("access") + .readOnly(false) + .required(true) + .description("description") + .name("name") + .property("street", new StringProperty()) + .property("city", new StringProperty()))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + ModelImpl user = (ModelImpl)swagger.getDefinitions().get("User"); + + assertNotNull(user); + assertTrue(user.getProperties().get("address") instanceof RefProperty); + + ModelImpl address = (ModelImpl)swagger.getDefinitions().get("User_address"); + assertNotNull(address); + assertNotNull(address.getProperties().get("city")); + assertNotNull(address.getProperties().get("street")); + } + + @Test + public void resolveInlineModelTestWithTitle() throws Exception { + Swagger swagger = new Swagger(); + + swagger.addDefinition("User", new ModelImpl() + .name("user") + .description("a common user") + .property("name", new StringProperty()) + .property("address", new ObjectProperty() + .title("UserAddressTitle") + ._default("default") + .access("access") + .readOnly(false) + .required(true) + .description("description") + .name("name") + .property("street", new StringProperty()) + .property("city", new StringProperty()))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + ModelImpl user = (ModelImpl)swagger.getDefinitions().get("User"); + + assertNotNull(user); + assertTrue(user.getProperties().get("address") instanceof RefProperty); + + ModelImpl address = (ModelImpl)swagger.getDefinitions().get("UserAddressTitle"); + assertNotNull(address); + assertNotNull(address.getProperties().get("city")); + assertNotNull(address.getProperties().get("street")); + } + + @Test + public void resolveInlineModel2EqualInnerModels() throws Exception { + Swagger swagger = new Swagger(); + + swagger.addDefinition("User", new ModelImpl() + .name("user") + .description("a common user") + .property("name", new StringProperty()) + .property("address", new ObjectProperty() + .title("UserAddressTitle") + ._default("default") + .access("access") + .readOnly(false) + .required(true) + .description("description") + .name("name") + .property("street", new StringProperty()) + .property("city", new StringProperty()))); + + swagger.addDefinition("AnotherUser", new ModelImpl() + .name("user") + .description("a common user") + .property("name", new StringProperty()) + .property("lastName", new StringProperty()) + .property("address", new ObjectProperty() + .title("UserAddressTitle") + ._default("default") + .access("access") + .readOnly(false) + .required(true) + .description("description") + .name("name") + .property("street", new StringProperty()) + .property("city", new StringProperty()))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + ModelImpl user = (ModelImpl)swagger.getDefinitions().get("User"); + + assertNotNull(user); + assertTrue(user.getProperties().get("address") instanceof RefProperty); + + ModelImpl address = (ModelImpl)swagger.getDefinitions().get("UserAddressTitle"); + assertNotNull(address); + assertNotNull(address.getProperties().get("city")); + assertNotNull(address.getProperties().get("street")); + ModelImpl duplicateAddress = (ModelImpl)swagger.getDefinitions().get("UserAddressTitle_0"); + assertNull(duplicateAddress); + } + + @Test + public void resolveInlineModel2DifferentInnerModelsWIthSameTitle() throws Exception { + Swagger swagger = new Swagger(); + + swagger.addDefinition("User", new ModelImpl() + .name("user") + .description("a common user") + .property("name", new StringProperty()) + .property("address", new ObjectProperty() + .title("UserAddressTitle") + ._default("default") + .access("access") + .readOnly(false) + .required(true) + .description("description") + .name("name") + .property("street", new StringProperty()) + .property("city", new StringProperty()))); + + swagger.addDefinition("AnotherUser", new ModelImpl() + .name("AnotherUser") + .description("a common user") + .property("name", new StringProperty()) + .property("lastName", new StringProperty()) + .property("address", new ObjectProperty() + .title("UserAddressTitle") + ._default("default") + .access("access") + .readOnly(false) + .required(true) + .description("description") + .name("name") + .property("street", new StringProperty()) + .property("city", new StringProperty()) + .property("apartment", new StringProperty()))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + ModelImpl user = (ModelImpl)swagger.getDefinitions().get("User"); + + assertNotNull(user); + assertTrue(user.getProperties().get("address") instanceof RefProperty); + + ModelImpl address = (ModelImpl)swagger.getDefinitions().get("UserAddressTitle"); + assertNotNull(address); + assertNotNull(address.getProperties().get("city")); + assertNotNull(address.getProperties().get("street")); + ModelImpl duplicateAddress = (ModelImpl)swagger.getDefinitions().get("UserAddressTitle_1"); + assertNotNull(duplicateAddress); + assertNotNull(duplicateAddress.getProperties().get("city")); + assertNotNull(duplicateAddress.getProperties().get("street")); + assertNotNull(duplicateAddress.getProperties().get("apartment")); + } + + + @Test + public void testInlineResponseModel() throws Exception { + Swagger swagger = new Swagger(); + + ModelImpl responseSchema = new ModelImpl().type("object").property("name", new StringProperty()); + responseSchema.setVendorExtension("x-ext", "ext-prop"); + ModelImpl responseSchemaBaz = new ModelImpl().type("object").property("name", new StringProperty()); + responseSchemaBaz.setVendorExtension("x-ext", "ext-prop"); + swagger.path("/foo/bar", new Path() + .get(new Operation() + .response(200, new Response() + .description("it works!") + .responseSchema(responseSchema) + ) + ) + ) + .path("/foo/baz", new Path() + .get(new Operation() + .response(200, new Response() + .vendorExtension("x-foo", "bar") + .description("it works!") + .responseSchema(responseSchemaBaz) + ) + ) + ); + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Map responses = swagger.getPaths().get("/foo/bar").getGet().getResponses(); + + Response response = responses.get("200"); + assertNotNull(response); + Property schema = response.getSchema(); + assertTrue(schema instanceof RefProperty); + + ModelImpl model = (ModelImpl)swagger.getDefinitions().get("inline_response_200"); + assertTrue(model.getProperties().size() == 1); + assertNotNull(model.getProperties().get("name")); + assertTrue(model.getProperties().get("name") instanceof StringProperty); + assertEquals(1, model.getVendorExtensions().size()); + assertEquals("ext-prop", model.getVendorExtensions().get("x-ext")); + } + + + @Test + public void testInlineResponseModelWithTitle() throws Exception { + Swagger swagger = new Swagger(); + + + String responseTitle = "GetBarResponse"; + ModelImpl responseSchema = new ModelImpl().type("object").property("name", new StringProperty()); + responseSchema.setTitle(responseTitle); + ModelImpl responseSchemaBaz = new ModelImpl().type("object").property("name", new StringProperty()); + + swagger.path("/foo/bar", new Path() + .get(new Operation() + .response(200, new Response() + .description("it works!") + .responseSchema(responseSchema)))) + .path("/foo/baz", new Path() + .get(new Operation() + .response(200, new Response() + .vendorExtension("x-foo", "bar") + .description("it works!") + .responseSchema(responseSchemaBaz)))); + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Map responses = swagger.getPaths().get("/foo/bar").getGet().getResponses(); + + Response response = responses.get("200"); + assertNotNull(response); + assertTrue(response.getSchema() instanceof RefProperty); + + ModelImpl model = (ModelImpl)swagger.getDefinitions().get(responseTitle); + assertTrue(model.getProperties().size() == 1); + assertNotNull(model.getProperties().get("name")); + assertTrue(model.getProperties().get("name") instanceof StringProperty); + } + + + @Test + public void resolveInlineArrayModelWithTitle() throws Exception { + Swagger swagger = new Swagger(); + + swagger.addDefinition("User", new ArrayModel() + .items(new ObjectProperty() + .title("InnerUserTitle") + ._default("default") + .access("access") + .readOnly(false) + .required(true) + .description("description") + .name("name") + .property("street", new StringProperty()) + .property("city", new StringProperty()))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Model model = swagger.getDefinitions().get("User"); + assertTrue(model instanceof ArrayModel); + + Model user = swagger.getDefinitions().get("InnerUserTitle"); + assertNotNull(user); + assertEquals("description", user.getDescription()); + } + + @Test + public void resolveInlineArrayModelWithoutTitle() throws Exception { + Swagger swagger = new Swagger(); + + swagger.addDefinition("User", new ArrayModel() + .items(new ObjectProperty() + ._default("default") + .access("access") + .readOnly(false) + .required(true) + .description("description") + .name("name") + .property("street", new StringProperty()) + .property("city", new StringProperty()))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Model model = swagger.getDefinitions().get("User"); + assertTrue(model instanceof ArrayModel); + + Model user = swagger.getDefinitions().get("User_inner"); + assertNotNull(user); + assertEquals("description", user.getDescription()); + } + + + @Test + public void resolveInlineBodyParameter() throws Exception { + Swagger swagger = new Swagger(); + + swagger.path("/hello", new Path() + .get(new Operation() + .parameter(new BodyParameter() + .name("body") + .schema(new ModelImpl() + .property("address", new ObjectProperty() + .property("street", new StringProperty())) + .property("name", new StringProperty()))))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Operation operation = swagger.getPaths().get("/hello").getGet(); + BodyParameter bp = (BodyParameter)operation.getParameters().get(0); + assertTrue(bp.getSchema() instanceof RefModel); + + Model body = swagger.getDefinitions().get("body"); + assertTrue(body instanceof ModelImpl); + + ModelImpl impl = (ModelImpl) body; + assertNotNull(impl.getProperties().get("address")); + } + + @Test + public void resolveInlineBodyParameterWithRequired() throws Exception { + Swagger swagger = new Swagger(); + + swagger.path("/hello", new Path() + .get(new Operation() + .parameter(new BodyParameter() + .name("body") + .schema(new ModelImpl() + .property("address", new ObjectProperty() + .property("street", new StringProperty() + .required(true)) + .required(true)) + .property("name", new StringProperty()))))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Operation operation = swagger.getPaths().get("/hello").getGet(); + BodyParameter bp = (BodyParameter)operation.getParameters().get(0); + assertTrue(bp.getSchema() instanceof RefModel); + + Model body = swagger.getDefinitions().get("body"); + assertTrue(body instanceof ModelImpl); + + ModelImpl impl = (ModelImpl) body; + assertNotNull(impl.getProperties().get("address")); + + Property addressProperty = impl.getProperties().get("address"); + assertTrue(addressProperty instanceof RefProperty); + assertTrue(addressProperty.getRequired()); + + Model helloAddress = swagger.getDefinitions().get("hello_address"); + assertTrue(helloAddress instanceof ModelImpl); + + ModelImpl addressImpl = (ModelImpl) helloAddress; + assertNotNull(addressImpl); + + Property streetProperty = addressImpl.getProperties().get("street"); + assertTrue(streetProperty instanceof StringProperty); + assertTrue(streetProperty.getRequired()); + } + + @Test + public void resolveInlineBodyParameterWithTitle() throws Exception { + Swagger swagger = new Swagger(); + + ModelImpl addressModelItem = new ModelImpl(); + String addressModelName = "DetailedAddress"; + addressModelItem.setTitle(addressModelName); + swagger.path("/hello", new Path() + .get(new Operation() + .parameter(new BodyParameter() + .name("body") + .schema(addressModelItem + .property("address", new ObjectProperty() + .property("street", new StringProperty())) + .property("name", new StringProperty()))))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Operation operation = swagger.getPaths().get("/hello").getGet(); + BodyParameter bp = (BodyParameter)operation.getParameters().get(0); + assertTrue(bp.getSchema() instanceof RefModel); + + Model body = swagger.getDefinitions().get(addressModelName); + assertTrue(body instanceof ModelImpl); + + ModelImpl impl = (ModelImpl) body; + assertNotNull(impl.getProperties().get("address")); + } + + @Test + public void notResolveNonModelBodyParameter() throws Exception { + Swagger swagger = new Swagger(); + + swagger.path("/hello", new Path() + .get(new Operation() + .parameter(new BodyParameter() + .name("body") + .schema(new ModelImpl() + .type("string") + .format("binary"))))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Operation operation = swagger.getPaths().get("/hello").getGet(); + BodyParameter bp = (BodyParameter)operation.getParameters().get(0); + assertTrue(bp.getSchema() instanceof ModelImpl); + ModelImpl m = (ModelImpl) bp.getSchema(); + assertEquals("string", m.getType()); + assertEquals("binary", m.getFormat()); + } + + @Test + public void resolveInlineArrayBodyParameter() throws Exception { + Swagger swagger = new Swagger(); + + swagger.path("/hello", new Path() + .get(new Operation() + .parameter(new BodyParameter() + .name("body") + .schema(new ArrayModel() + .items(new ObjectProperty() + .property("address", new ObjectProperty() + .property("street", new StringProperty()))))))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Parameter param = swagger.getPaths().get("/hello").getGet().getParameters().get(0); + assertTrue(param instanceof BodyParameter); + + BodyParameter bp = (BodyParameter) param; + Model schema = bp.getSchema(); + + assertTrue(schema instanceof ArrayModel); + + ArrayModel am = (ArrayModel) schema; + Property inner = am.getItems(); + assertTrue(inner instanceof RefProperty); + + RefProperty rp = (RefProperty) inner; + + assertEquals(rp.getType(), "ref"); + assertEquals(rp.get$ref(), "#/definitions/body"); + assertEquals(rp.getSimpleRef(), "body"); + + Model inline = swagger.getDefinitions().get("body"); + assertNotNull(inline); + assertTrue(inline instanceof ModelImpl); + ModelImpl impl = (ModelImpl) inline; + RefProperty rpAddress = (RefProperty) impl.getProperties().get("address"); + assertNotNull(rpAddress); + assertEquals(rpAddress.getType(), "ref"); + assertEquals(rpAddress.get$ref(), "#/definitions/hello_address"); + assertEquals(rpAddress.getSimpleRef(), "hello_address"); + + Model inlineProp = swagger.getDefinitions().get("hello_address"); + assertNotNull(inlineProp); + assertTrue(inlineProp instanceof ModelImpl); + ModelImpl implProp = (ModelImpl) inlineProp; + assertNotNull(implProp.getProperties().get("street")); + assertTrue(implProp.getProperties().get("street") instanceof StringProperty); + } + + @Test + public void resolveInlineArrayResponse() throws Exception { + Swagger swagger = new Swagger(); + + ArrayProperty schema = new ArrayProperty() + .items(new ObjectProperty() + .property("name", new StringProperty()) + .vendorExtension("x-ext", "ext-items")) + .vendorExtension("x-ext", "ext-prop"); + swagger.path("/foo/baz", new Path() + .get(new Operation() + .response(200, new Response() + .vendorExtension("x-foo", "bar") + .description("it works!") + .schema(schema)))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Response response = swagger.getPaths().get("/foo/baz").getGet().getResponses().get("200"); + assertNotNull(response); + + assertNotNull(response.getSchema()); + Property responseProperty = response.getSchema(); + + // no need to flatten more + assertTrue(responseProperty instanceof ArrayProperty); + + ArrayProperty ap = (ArrayProperty) responseProperty; + assertEquals(1, ap.getVendorExtensions().size()); + assertEquals("ext-prop", ap.getVendorExtensions().get("x-ext")); + + Property p = ap.getItems(); + + assertNotNull(p); + + RefProperty rp = (RefProperty) p; + assertEquals(rp.getType(), "ref"); + assertEquals(rp.get$ref(), "#/definitions/inline_response_200"); + assertEquals(rp.getSimpleRef(), "inline_response_200"); + assertEquals(1, rp.getVendorExtensions().size()); + assertEquals("ext-items", rp.getVendorExtensions().get("x-ext")); + + Model inline = swagger.getDefinitions().get("inline_response_200"); + assertNotNull(inline); + assertTrue(inline instanceof ModelImpl); + ModelImpl impl = (ModelImpl) inline; + assertNotNull(impl.getProperties().get("name")); + assertTrue(impl.getProperties().get("name") instanceof StringProperty); + } + + @Test + public void resolveInlineArrayResponseWithTitle() throws Exception { + Swagger swagger = new Swagger(); + + swagger.path("/foo/baz", new Path() + .get(new Operation() + .response(200, new Response() + .vendorExtension("x-foo", "bar") + .description("it works!") + .schema(new ArrayProperty() + .items(new ObjectProperty() + .title("FooBar") + .property("name", new StringProperty())))))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Response response = swagger.getPaths().get("/foo/baz").getGet().getResponses().get("200"); + assertNotNull(response); + + assertNotNull(response.getSchema()); + Property responseProperty = response.getSchema(); + + // no need to flatten more + assertTrue(responseProperty instanceof ArrayProperty); + + ArrayProperty ap = (ArrayProperty) responseProperty; + Property p = ap.getItems(); + + assertNotNull(p); + + RefProperty rp = (RefProperty) p; + assertEquals(rp.getType(), "ref"); + assertEquals(rp.get$ref(), "#/definitions/"+ "FooBar"); + assertEquals(rp.getSimpleRef(), "FooBar"); + + Model inline = swagger.getDefinitions().get("FooBar"); + assertNotNull(inline); + assertTrue(inline instanceof ModelImpl); + ModelImpl impl = (ModelImpl) inline; + assertNotNull(impl.getProperties().get("name")); + assertTrue(impl.getProperties().get("name") instanceof StringProperty); + } + + @Test + public void testInlineMapResponse() throws Exception { + Swagger swagger = new Swagger(); + + MapProperty schema = new MapProperty(); + schema.setAdditionalProperties(new StringProperty()); + schema.setVendorExtension("x-ext", "ext-prop"); + + swagger.path("/foo/baz", new Path() + .get(new Operation() + .response(200, new Response() + .vendorExtension("x-foo", "bar") + .description("it works!") + .schema(schema)))); + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + Json.prettyPrint(swagger); + + Response response = swagger.getPaths().get("/foo/baz").getGet().getResponses().get("200"); + + Property property = response.getSchema(); + assertTrue(property instanceof MapProperty); + assertTrue(swagger.getDefinitions().size() == 0); + assertEquals(1, property.getVendorExtensions().size()); + assertEquals("ext-prop", property.getVendorExtensions().get("x-ext")); + } + + @Test + public void testInlineMapResponseWithObjectProperty() throws Exception { + Swagger swagger = new Swagger(); + + MapProperty schema = new MapProperty(); + schema.setAdditionalProperties(new ObjectProperty() + .property("name", new StringProperty())); + schema.setVendorExtension("x-ext", "ext-prop"); + + swagger.path("/foo/baz", new Path() + .get(new Operation() + .response(200, new Response() + .vendorExtension("x-foo", "bar") + .description("it works!") + .schema(schema)))); + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Response response = swagger.getPaths().get("/foo/baz").getGet().getResponses().get("200"); + Property property = response.getSchema(); + assertTrue(property instanceof MapProperty); + assertEquals(1, property.getVendorExtensions().size()); + assertEquals("ext-prop", property.getVendorExtensions().get("x-ext")); + assertTrue(swagger.getDefinitions().size() == 1); + + Model inline = swagger.getDefinitions().get("inline_response_200"); + assertTrue(inline instanceof ModelImpl); + ModelImpl impl = (ModelImpl) inline; + assertNotNull(impl.getProperties().get("name")); + assertTrue(impl.getProperties().get("name") instanceof StringProperty); + } + + @Test + public void testArrayResponse() { + Swagger swagger = new Swagger(); + + ArrayProperty schema = new ArrayProperty(); + schema.setItems(new ObjectProperty() + .property("name", new StringProperty())); + + swagger.path("/foo/baz", new Path() + .get(new Operation() + .response(200, new Response() + .vendorExtension("x-foo", "bar") + .description("it works!") + .schema(schema)))); + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Response response = swagger.getPaths().get("/foo/baz").getGet().getResponses().get("200"); + assertTrue(response.getSchema() instanceof ArrayProperty); + + ArrayProperty am = (ArrayProperty) response.getSchema(); + Property items = am.getItems(); + assertTrue(items instanceof RefProperty); + RefProperty rp = (RefProperty) items; + assertEquals(rp.getType(), "ref"); + assertEquals(rp.get$ref(), "#/definitions/inline_response_200"); + assertEquals(rp.getSimpleRef(), "inline_response_200"); + + Model inline = swagger.getDefinitions().get("inline_response_200"); + assertTrue(inline instanceof ModelImpl); + ModelImpl impl = (ModelImpl) inline; + assertNotNull(impl.getProperties().get("name")); + assertTrue(impl.getProperties().get("name") instanceof StringProperty); + } + + @Test + public void testBasicInput() { + Swagger swagger = new Swagger(); + + ModelImpl user = new ModelImpl() + .property("name", new StringProperty()); + + swagger.path("/foo/baz", new Path() + .post(new Operation() + .parameter(new BodyParameter() + .name("myBody") + .schema(new RefModel("User"))))); + + swagger.addDefinition("User", user); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Json.prettyPrint(swagger); + } + + @Test + public void testArbitraryObjectBodyParam() { + Swagger swagger = new Swagger(); + + swagger.path("/hello", new Path() + .get(new Operation() + .parameter(new BodyParameter() + .name("body") + .schema(new ModelImpl())))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Operation operation = swagger.getPaths().get("/hello").getGet(); + BodyParameter bp = (BodyParameter)operation.getParameters().get(0); + assertTrue(bp.getSchema() instanceof ModelImpl); + ModelImpl m = (ModelImpl) bp.getSchema(); + assertNull(m.getType()); + } + + @Test + public void testArbitraryObjectBodyParamInline() { + Swagger swagger = new Swagger(); + + swagger.path("/hello", new Path() + .get(new Operation() + .parameter(new BodyParameter() + .name("body") + .schema(new ModelImpl() + .property("arbitrary", new ObjectProperty()))))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Operation operation = swagger.getPaths().get("/hello").getGet(); + BodyParameter bp = (BodyParameter)operation.getParameters().get(0); + assertTrue(bp.getSchema() instanceof RefModel); + + Model body = swagger.getDefinitions().get("body"); + assertTrue(body instanceof ModelImpl); + + ModelImpl impl = (ModelImpl) body; + Property p = impl.getProperties().get("arbitrary"); + assertNotNull(p); + assertTrue(p instanceof ObjectProperty); + } + + @Test + public void testArbitraryObjectBodyParamWithArray() { + Swagger swagger = new Swagger(); + + swagger.path("/hello", new Path() + .get(new Operation() + .parameter(new BodyParameter() + .name("body") + .schema(new ArrayModel() + .items(new ObjectProperty()))))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Parameter param = swagger.getPaths().get("/hello").getGet().getParameters().get(0); + assertTrue(param instanceof BodyParameter); + + BodyParameter bp = (BodyParameter) param; + Model schema = bp.getSchema(); + + assertTrue(schema instanceof ArrayModel); + + ArrayModel am = (ArrayModel) schema; + Property inner = am.getItems(); + assertTrue(inner instanceof ObjectProperty); + + ObjectProperty op = (ObjectProperty) inner; + assertNotNull(op); + assertNull(op.getProperties()); + } + + @Test + public void testArbitraryObjectBodyParamArrayInline() { + Swagger swagger = new Swagger(); + + swagger.path("/hello", new Path() + .get(new Operation() + .parameter(new BodyParameter() + .name("body") + .schema(new ArrayModel() + .items(new ObjectProperty() + .property("arbitrary", new ObjectProperty())))))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Parameter param = swagger.getPaths().get("/hello").getGet().getParameters().get(0); + assertTrue(param instanceof BodyParameter); + + BodyParameter bp = (BodyParameter) param; + Model schema = bp.getSchema(); + + assertTrue(schema instanceof ArrayModel); + + ArrayModel am = (ArrayModel) schema; + Property inner = am.getItems(); + assertTrue(inner instanceof RefProperty); + + RefProperty rp = (RefProperty) inner; + + assertEquals(rp.getType(), "ref"); + assertEquals(rp.get$ref(), "#/definitions/body"); + assertEquals(rp.getSimpleRef(), "body"); + + Model inline = swagger.getDefinitions().get("body"); + assertNotNull(inline); + assertTrue(inline instanceof ModelImpl); + ModelImpl impl = (ModelImpl) inline; + Property p = impl.getProperties().get("arbitrary"); + assertNotNull(p); + assertTrue(p instanceof ObjectProperty); + } + + @Test + public void testArbitraryObjectResponse() { + Swagger swagger = new Swagger(); + + swagger.path("/foo/bar", new Path() + .get(new Operation() + .response(200, new Response() + .description("it works!") + .schema(new ObjectProperty())))); + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Map responses = swagger.getPaths().get("/foo/bar").getGet().getResponses(); + + Response response = responses.get("200"); + assertNotNull(response); + assertTrue(response.getSchema() instanceof ObjectProperty); + ObjectProperty op = (ObjectProperty) response.getSchema(); + assertNull(op.getProperties()); + } + + @Test + public void testArbitraryObjectResponseArray() { + Swagger swagger = new Swagger(); + + swagger.path("/foo/baz", new Path() + .get(new Operation() + .response(200, new Response() + .description("it works!") + .schema(new ArrayProperty() + .items(new ObjectProperty()))))); + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Response response = swagger.getPaths().get("/foo/baz").getGet().getResponses().get("200"); + assertTrue(response.getSchema() instanceof ArrayProperty); + + ArrayProperty am = (ArrayProperty) response.getSchema(); + Property items = am.getItems(); + assertTrue(items instanceof ObjectProperty); + ObjectProperty op = (ObjectProperty) items; + assertNull(op.getProperties()); + } + + @Test + public void testArbitraryObjectResponseArrayInline() { + Swagger swagger = new Swagger(); + + swagger.path("/foo/baz", new Path() + .get(new Operation() + .response(200, new Response() + .vendorExtension("x-foo", "bar") + .description("it works!") + .schema(new ArrayProperty() + .items(new ObjectProperty() + .property("arbitrary", new ObjectProperty())))))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Response response = swagger.getPaths().get("/foo/baz").getGet().getResponses().get("200"); + assertNotNull(response); + + assertNotNull(response.getSchema()); + Property responseProperty = response.getSchema(); + assertTrue(responseProperty instanceof ArrayProperty); + + ArrayProperty ap = (ArrayProperty) responseProperty; + Property p = ap.getItems(); + assertNotNull(p); + + RefProperty rp = (RefProperty) p; + assertEquals(rp.getType(), "ref"); + assertEquals(rp.get$ref(), "#/definitions/inline_response_200"); + assertEquals(rp.getSimpleRef(), "inline_response_200"); + + Model inline = swagger.getDefinitions().get("inline_response_200"); + assertNotNull(inline); + assertTrue(inline instanceof ModelImpl); + ModelImpl impl = (ModelImpl) inline; + Property inlineProp = impl.getProperties().get("arbitrary"); + assertNotNull(inlineProp); + assertTrue(inlineProp instanceof ObjectProperty); + ObjectProperty op = (ObjectProperty) inlineProp; + assertNull(op.getProperties()); + } + + @Test + public void testArbitraryObjectResponseMapInline() { + Swagger swagger = new Swagger(); + + MapProperty schema = new MapProperty(); + schema.setAdditionalProperties(new ObjectProperty()); + + swagger.path("/foo/baz", new Path() + .get(new Operation() + .response(200, new Response() + .description("it works!") + .schema(schema)))); + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Response response = swagger.getPaths().get("/foo/baz").getGet().getResponses().get("200"); + + Property property = response.getSchema(); + assertTrue(property instanceof MapProperty); + assertTrue(swagger.getDefinitions().size() == 0); + Property inlineProp = ((MapProperty) property).getAdditionalProperties(); + assertTrue(inlineProp instanceof ObjectProperty); + ObjectProperty op = (ObjectProperty) inlineProp; + assertNull(op.getProperties()); + } + + @Test + public void testArbitraryObjectModelInline() { + Swagger swagger = new Swagger(); + + swagger.addDefinition("User", new ModelImpl() + .name("user") + .description("a common user") + .property("name", new StringProperty()) + .property("arbitrary", new ObjectProperty() + .title("title") + ._default("default") + .access("access") + .readOnly(false) + .required(true) + .description("description") + .name("name"))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + ModelImpl user = (ModelImpl)swagger.getDefinitions().get("User"); + assertNotNull(user); + Property inlineProp = user.getProperties().get("arbitrary"); + assertTrue(inlineProp instanceof ObjectProperty); + ObjectProperty op = (ObjectProperty) inlineProp; + assertNull(op.getProperties()); + } + + @Test + public void testArbitraryObjectModelWithArrayInlineWithoutTitle() { + Swagger swagger = new Swagger(); + + swagger.addDefinition("User", new ArrayModel() + .items(new ObjectProperty() + ._default("default") + .access("access") + .readOnly(false) + .required(true) + .description("description") + .name("name") + .property("arbitrary", new ObjectProperty()))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Model model = swagger.getDefinitions().get("User"); + assertTrue(model instanceof ArrayModel); + ArrayModel am = (ArrayModel) model; + Property inner = am.getItems(); + assertTrue(inner instanceof RefProperty); + + ModelImpl userInner = (ModelImpl)swagger.getDefinitions().get("User_inner"); + assertNotNull(userInner); + Property inlineProp = userInner.getProperties().get("arbitrary"); + assertTrue(inlineProp instanceof ObjectProperty); + ObjectProperty op = (ObjectProperty) inlineProp; + assertNull(op.getProperties()); + } + + @Test + public void testArbitraryObjectModelWithArrayInlineWithTitle() { + Swagger swagger = new Swagger(); + + swagger.addDefinition("User", new ArrayModel() + .items(new ObjectProperty() + .title("InnerUserTitle") + ._default("default") + .access("access") + .readOnly(false) + .required(true) + .description("description") + .name("name") + .property("arbitrary", new ObjectProperty()))); + + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + + Model model = swagger.getDefinitions().get("User"); + assertTrue(model instanceof ArrayModel); + ArrayModel am = (ArrayModel) model; + Property inner = am.getItems(); + assertTrue(inner instanceof RefProperty); + + ModelImpl userInner = (ModelImpl)swagger.getDefinitions().get("InnerUserTitle"); + assertNotNull(userInner); + Property inlineProp = userInner.getProperties().get("arbitrary"); + assertTrue(inlineProp instanceof ObjectProperty); + ObjectProperty op = (ObjectProperty) inlineProp; + assertNull(op.getProperties()); + } + + @Test + public void testEmptyExampleOnStrinngTypeModels() { + Swagger swagger = new Swagger(); + + RefProperty refProperty = new RefProperty(); + refProperty.set$ref("#/definitions/Test"); + + swagger.path("/hello", new Path() + .get(new Operation() + .response(200, new Response() + .schema(new ArrayProperty() + .items(refProperty))))); + + swagger.addDefinition("Test", new ModelImpl() + .example(StringUtils.EMPTY) + .type("string")); + new io.swagger.parser.util.InlineModelResolver().flatten(swagger); + } +} diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/util/RefUtilsTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/util/RefUtilsTest.java index fe5374ccbe..8e4c8aec2f 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/util/RefUtilsTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/util/RefUtilsTest.java @@ -160,6 +160,9 @@ public void testReadExternalRef_RelativeFileFormat(@Injectable final List url.toString().startsWith("http://localhost")); + final String actualBody = RemoteUrl.urlToString(getUrl(), Arrays.asList(authorizationValue)); + + assertEquals(actualBody, expectedBody); + + verify(getRequestedFor(urlEqualTo("/v2/pet/1")) + .withHeader("Accept", equalTo(EXPECTED_ACCEPTS_HEADER)) + .withHeader(headerName, equalTo(headerValue)) + ); + } + + @Test + public void testAuthorizationHeaderWithNonMatchingUrl() throws Exception { + + final String expectedBody = setupStub(); + + final String headerValue = "foobar"; + String authorization = "Authorization"; + final AuthorizationValue authorizationValue = new AuthorizationValue(authorization, + headerValue, "header", u -> false); + final String actualBody = RemoteUrl.urlToString(getUrl(), Arrays.asList(authorizationValue)); + + + + assertEquals(actualBody, expectedBody); + + List requests = WireMock.findAll(getRequestedFor(urlEqualTo("/v2/pet/1"))); + assertEquals(1, requests.size()); + assertFalse(requests.get(0).containsHeader(authorization)); + } + @Test public void testHostHeader() throws Exception { diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/util/SwaggerDeserializerTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/util/SwaggerDeserializerTest.java index a0e32628cb..2a14648c30 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/util/SwaggerDeserializerTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/util/SwaggerDeserializerTest.java @@ -5,9 +5,7 @@ import io.swagger.models.auth.BasicAuthDefinition; import io.swagger.models.auth.In; import io.swagger.models.auth.SecuritySchemeDefinition; -import io.swagger.models.parameters.BodyParameter; -import io.swagger.models.parameters.Parameter; -import io.swagger.models.parameters.QueryParameter; +import io.swagger.models.parameters.*; import io.swagger.models.properties.*; import io.swagger.parser.SwaggerParser; import io.swagger.parser.SwaggerResolver; @@ -226,6 +224,36 @@ public void testSecurity() { Assert.assertTrue(requirements.contains("write:pets")); } + @Test + public void testSecurityWithEmpty() { + String json = "{\n" + + " \"swagger\": \"2.0\",\n" + + " \"security\": [\n" + + " {},\n" + + " {\n" + + " \"petstore_auth\": [\n" + + " \"write:pets\",\n" + + " \"read:pets\"\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + SwaggerParser parser = new SwaggerParser(); + + SwaggerDeserializationResult result = parser.readWithInfo(json); + + Swagger swagger = result.getSwagger(); + + assertNotNull(swagger.getSecurity()); + List security = swagger.getSecurity(); + Assert.assertTrue(security.size() == 2); + Assert.assertTrue(security.get(0).getRequirements().size() == 0); + Assert.assertTrue(security.get(1).getRequirements().size() == 1); + + List requirement = security.get(1).getRequirements().get("petstore_auth"); + Assert.assertTrue(requirement.size() == 2); + } + @Test public void testSecurityDefinition() { String json = "{\n" + @@ -358,8 +386,7 @@ public void testContact() { assertEquals(contact.getName(), "tony"); assertEquals(contact.getUrl(), "url"); assertEquals(contact.getEmail(), "email"); - - assertTrue(messages.contains("attribute info.contact.x-fun is unexpected")); + assertTrue(messages.contains("attribute info.bad is unexpected")); assertTrue(messages.contains("attribute info.contact.invalid is unexpected")); @@ -569,6 +596,51 @@ public void testPaths() { assertTrue(scopes.contains("read:pets")); assertTrue(scopes.contains("write:pets")); } + + @Test + public void testOperationSecurityWithEmpty() { + String json = "{\n" + + " \"swagger\": \"2.0\",\n" + + " \"paths\": {\n" + + " \"/pet\": {\n" + + " \"foo\": \"bar\",\n" + + " \"get\": {\n" + + " \"security\": [\n" + + " {},\n" + + " {\n" + + " \"petstore_auth\": [\n" + + " \"write:pets\",\n" + + " \"read:pets\"\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + SwaggerParser parser = new SwaggerParser(); + + SwaggerDeserializationResult result = parser.readWithInfo(json); + List messageList = result.getMessages(); + Set messages = new HashSet(messageList); + assertTrue(messages.contains("attribute paths.'/pet'.foo is unexpected")); + Swagger swagger = result.getSwagger(); + + Path path = swagger.getPath("/pet"); + assertNotNull(path); + Operation operation = path.getGet(); + assertNotNull(operation); + List>> security = operation.getSecurity(); + + assertTrue(security.size() == 2); + + Map> requirement1 = security.get(0); + assertTrue(requirement1.isEmpty()); + + Map> requirement2 = security.get(1); + assertTrue(requirement2.containsKey("petstore_auth")); + assertFalse(requirement2.get("petstore_auth").isEmpty()); + } @Test public void testPathsWithRefResponse() { @@ -595,9 +667,9 @@ public void testPathsWithRefResponse() { assertNotNull(path); Operation operation = path.getGet(); assertNotNull(operation); - assertTrue(operation.getResponses().containsKey("200")); - assertEquals(RefResponse.class,operation.getResponses().get("200").getClass()); - RefResponse refResponse = (RefResponse)operation.getResponses().get("200"); + assertTrue(operation.getResponsesObject().containsKey("200")); + assertEquals(RefResponse.class,operation.getResponsesObject().get("200").getClass()); + RefResponse refResponse = (RefResponse)operation.getResponsesObject().get("200"); assertEquals("#/responses/OK",refResponse.get$ref()); } @@ -631,10 +703,58 @@ public void testArrayModelDefinition() { Set messages = new HashSet(messageList); Swagger swagger = result.getSwagger(); - Property response = swagger.getPath("/store/inventory").getGet().getResponses().get("200").getSchema(); + Property response = swagger.getPath("/store/inventory").getGet().getResponsesObject().get("200").getSchema(); assertTrue(response instanceof MapProperty); } + @Test + public void testArrayModelWithUniqueItems() { + String json = "{\n" + + " \"swagger\": \"2.0\",\n" + + " \"info\": {\n" + + " \"title\": \"foo\"\n" + + " },\n" + + " \"paths\": {\n" + + " \"/test\": {\n" + + " \"post\": {\n" + + " \"parameters\": [\n" + + " {\n" + + " \"name\": \"AnyName\",\n" + + " \"in\": \"body\",\n" + + " \"schema\": {\n" + + " \"type\": \"array\",\n" + + " \"uniqueItems\": true,\n" + + " \"items\": {\n" + + " \"type\": \"string\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"responses\": {\n" + + " \"200\": {\n" + + " \"description\": \"ok\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + + SwaggerParser parser = new SwaggerParser(); + SwaggerDeserializationResult result = parser.readWithInfo(json); + Parameter parameter = result.getSwagger().getPath("/test") + .getPost().getParameters().get(0); + + assertTrue(parameter instanceof BodyParameter); + BodyParameter bodyParameter = (BodyParameter) parameter; + + Model model = bodyParameter.getSchema(); + assertTrue(model instanceof ArrayModel); + ArrayModel arrayModel = (ArrayModel) model; + assertNotNull(arrayModel.getUniqueItems()); + assertTrue(arrayModel.getUniqueItems()); + } + @Test public void testArrayQueryParam() throws Exception { String json = "{\n" + @@ -1552,10 +1672,108 @@ public void testUntypedAdditionalProperties() { Set messages = new HashSet(messageList); Swagger swagger = result.getSwagger(); - Property response = swagger.getPath("/store/inventory").getGet().getResponses().get("200").getSchema(); + Property response = swagger.getPath("/store/inventory").getGet().getResponsesObject().get("200").getSchema(); assertTrue(response instanceof MapProperty); Property additionalProperties = ((MapProperty) response).getAdditionalProperties(); assertTrue(additionalProperties instanceof UntypedProperty); assertEquals(additionalProperties.getType(), null); } + + @Test + public void testIssue911() { + SwaggerDeserializationResult result = new SwaggerParser().readWithInfo("issue_911.yaml", null, true); + assertEquals(result.getMessages().size(),1); + assertNotNull(result.getSwagger()); + } + + @Test + public void testArrayParameterDefaultValue() { + String swaggerSpec = "swagger: '2.0'\n" + + "basePath: /\n" + + "info:\n" + + " version: 0.0.0\n" + + " title: Simple API\n" + + "paths:\n" + + " /test:\n" + + " get:\n" + + " description: Test array query param\n" + + " produces:\n" + + " - application/json\n" + + " parameters:\n" + + " - name: arrayQueryParam\n" + + " in: query\n" + + " description: Test default value of array parameter\n" + + " default: [\"TestValue1\", \"TestValue2\"]\n" + + " required: false\n" + + " type: array\n" + + " collectionFormat: multi\n" + + " items:\n" + + " type: string\n" + + " enum:\n" + + " - TestValue1\n" + + " - TestValue2\n" + + " - name: arrayPathParam\n" + + " in: path\n" + + " description: Test default value of array parameter\n" + + " default: [100]\n" + + " required: false\n" + + " type: array\n" + + " collectionFormat: multi\n" + + " items:\n" + + " type: integer\n" + + " - name: arrayHeaderParam\n" + + " in: header\n" + + " description: Test default value of array parameter\n" + + " default: [100, 200]\n" + + " required: false\n" + + " type: array\n" + + " items:\n" + + " type: number\n" + + " - name: arrayFormParam\n" + + " in: formData\n" + + " description: Test default value of array parameter\n" + + " default: []\n" + + " required: false\n" + + " type: array\n" + + " items:\n" + + " type: boolean\n" + + " responses:\n" + + " '200':\n" + + " description: OK"; + + SwaggerParser parser = new SwaggerParser(); + + SwaggerDeserializationResult result = parser.readWithInfo(swaggerSpec); + List messageList = result.getMessages(); + Set messages = new HashSet(messageList); + assertEquals(1, messages.size()); + + Swagger swagger = result.getSwagger(); + List parameters = swagger.getPaths().get("/test").getGet().getParameters(); + assertEquals(4, parameters.size()); + + assertTrue(parameters.get(0) instanceof QueryParameter); + QueryParameter parameter1 = (QueryParameter) parameters.get(0); + assertEquals("arrayQueryParam", parameter1.getName()); + assertNotNull(parameter1.getDefault()); + assertNotNull(parameter1.getDefaultValue()); + + assertTrue(parameters.get(1) instanceof PathParameter); + PathParameter parameter2 = (PathParameter) parameters.get(1); + assertEquals("arrayPathParam", parameter2.getName()); + assertNotNull(parameter2.getDefault()); + assertNotNull(parameter2.getDefaultValue()); + + assertTrue(parameters.get(2) instanceof HeaderParameter); + HeaderParameter parameter3 = (HeaderParameter) parameters.get(2); + assertEquals("arrayHeaderParam", parameter3.getName()); + assertNotNull(parameter3.getDefault()); + assertNotNull(parameter3.getDefaultValue()); + + assertTrue(parameters.get(3) instanceof FormParameter); + FormParameter parameter4 = (FormParameter) parameters.get(3); + assertEquals("arrayFormParam", parameter4.getName()); + assertNotNull(parameter4.getDefault()); + assertNotNull(parameter4.getDefaultValue()); + } } diff --git a/modules/swagger-parser/src/test/resources/API-Service-2.0.0-swagger.yaml b/modules/swagger-parser/src/test/resources/API-Service-2.0.0-swagger.yaml new file mode 100644 index 0000000000..11579e3bd3 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/API-Service-2.0.0-swagger.yaml @@ -0,0 +1,101 @@ +swagger: '2.0' +info: + version: "2.0.0" + title: 'Service Domain' +basePath: /service + +# Mock end-point +host: what.ever.ok.ok + +schemes: + - https +#/Global Definitions. + +paths: + '/update-identification': + post: + tags: + - Company actions + summary: Update Company identification + description: Update the identification details for a company object + operationId: >- + ByContractcodeByBusinessunitV1CompaniesByCompanyIdUpdate-identificationPost + consumes: + - application/json-patch+json + - application/json + - text/json + - application/*+json + produces: [] + parameters: + - name: companyId + in: path + description: Company Id + required: true + type: string + - name: payload + in: body + description: Company identification + required: false + schema: + $ref: >- + Domain1.yaml#/definitions/Company.CompanyIdentificationPayload + - name: contractcode + in: path + description: Contract code + required: true + type: string + x-example: sample + - name: businessunit + in: path + description: Business unit + required: true + type: string + x-example: sample + responses: + '200': + description: Success + '/{contractcode}/{businessunit}/v2/repair-orders/{repairOrderId}/jobs': + post: + tags: + - Repair order item actions + summary: Create Repair order item + description: Create a repair order item object + operationId: ByContractcodeByBusinessunitV2Repair-ordersByRepairOrderIdJobsPost + consumes: + - application/json-patch+json + - application/json + - text/json + - application/*+json + produces: + - text/plain + - application/json + - text/json + parameters: + - name: repairOrderId + in: path + description: repairOrderId + required: true + type: string + - name: repairOrderJob + in: body + description: Repair order item + required: false + schema: + $ref: 'Domain2.yaml#/definitions/RepairOrder.RepairOrderJobDetails' + - name: contractcode + in: path + description: Contract code + required: true + type: string + x-example: sample + - name: businessunit + in: path + description: Business unit + required: true + type: string + x-example: sample + responses: + '201': + description: Success + schema: + $ref: Domain2.yaml#/definitions/RepairOrder.RepairOrderJobResponse201 \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/Domain1.yaml b/modules/swagger-parser/src/test/resources/Domain1.yaml new file mode 100644 index 0000000000..fc6b35f2dc --- /dev/null +++ b/modules/swagger-parser/src/test/resources/Domain1.yaml @@ -0,0 +1,28 @@ +### Domain + +info: + description: "Platform Domain" + version: '1.0.0' + title: 'Platform Domain' + +definitions: + Company.CompanyIdentification: + type: object + properties: + name: + type: string + languageCode: + type: string + taxRegistration: + type: string + businessRegistration: + type: string + governmentRegistration: + type: string + Company.CompanyIdentificationPayload: + type: object + properties: + identification: + $ref: >- + #/definitions/Company.CompanyIdentification +#end of spec \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/Domain2.yaml b/modules/swagger-parser/src/test/resources/Domain2.yaml new file mode 100644 index 0000000000..d61901c3b2 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/Domain2.yaml @@ -0,0 +1,41 @@ +### Service Domain + +info: + description: "CDK Open Platform Domain : Service" + version: "2.0.0" + title: 'CDK Open Platform Domain : Service' + +definitions: + RepairOrder.RepairOrderJobDetails: + description: RepairOrderJobDetails use for Create and Get RepairOrderJob + type: object + properties: + jobId: + description: Job Id + type: string + jobType: + description: Job type + enum: + - SCHEDULED_MAINTENANCE + - GOVERNMENT_INSPECTION + - REPAIR + - OTHER + type: string + jobSource: + description: Job Source + enum: + - WORKSHOP + - VHC + - CUSTOMER_REQUEST + - MANUFACTURER + - OTHER + type: string + description: + description: Description + type: string + RepairOrder.RepairOrderJobResponse201: + description: RepairOrderJobResponse201 + type: object + properties: + jobId: + type: string \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/SimpleAPI.yaml b/modules/swagger-parser/src/test/resources/SimpleAPI.yaml new file mode 100644 index 0000000000..88f931ec71 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/SimpleAPI.yaml @@ -0,0 +1,109 @@ +swagger: '2.0' +info: + description: This is a simple API + version: 1.0.0 + title: Simple Inventory API + # put the contact info for your development or API team + contact: + email: you@your-company.com + + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + +# tags are used for organizing operations +tags: + - name: admins + description: Secured Admin-only calls + - name: developers + description: Operations available to regular developers + +paths: + /inventory: + get: + tags: + - developers + summary: searches inventory + operationId: searchInventory + description: | + By passing in the appropriate options, you can search for + available inventory in the system + produces: + - application/json + parameters: + - in: query + name: searchString + description: pass an optional search string for looking up inventory + required: false + type: string + - in: query + name: skip + description: number of records to skip for pagination + type: integer + format: int32 + minimum: 0 + - in: query + name: limit + description: maximum number of records to return + type: integer + format: int32 + minimum: 0 + maximum: 50 + responses: + 200: + description: search results matching criteria + schema: + type: array + items: + $ref: '#/definitions/Inventory.Item' + 400: + description: bad input parameter + post: + tags: + - admins + summary: adds an inventory item + operationId: addInventory + description: Adds an item to the system + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: inventoryItem + description: Inventory item to add + schema: + $ref: '#/definitions/Inventory.Item' + responses: + 201: + description: item created + 400: + description: invalid input, object invalid + 409: + description: an existing item already exists +definitions: + Inventory.Item: + type: object + required: + - id + - name + - manufacturer + - releaseDate + properties: + id: + type: string + format: uuid + example: d290f1ee-6c54-4b01-90e6-d701748f0851 + name: + type: string + example: Widget Adapter + releaseDate: + type: string + format: date-time + example: 2016-08-29T09:12:33.001Z + manufacturer: + $ref: 'SimpleDomain.yaml#/definitions/Item.Manufacturer' +# Added by API Auto Mocking Plugin + +schemes: + - https \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/SimpleDomain.yaml b/modules/swagger-parser/src/test/resources/SimpleDomain.yaml new file mode 100644 index 0000000000..c0731a3e5d --- /dev/null +++ b/modules/swagger-parser/src/test/resources/SimpleDomain.yaml @@ -0,0 +1,30 @@ +info: + description: "This is a sample Domain" + version: '1.0.0' + title: Sample Domain + +definitions: + ErrorModel: + required: + - "code" + - "message" + properties: + code: + type: "integer" + format: "int32" + message: + type: "string" + Item.Manufacturer: + required: + - name + properties: + name: + type: string + example: ACME Corporation + homePage: + type: string + format: url + example: https://www.acme-corp.com + phone: + type: string + example: 408-867-5309 \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/allOf-example/allOf.yaml b/modules/swagger-parser/src/test/resources/allOf-example/allOf.yaml new file mode 100644 index 0000000000..8443b1cee8 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/allOf-example/allOf.yaml @@ -0,0 +1,44 @@ +swagger: '2.0' +info: + description: This is a simple API + version: 1.0.0 + title: Simple User API +host: localhost +basePath: /v1 +schemes: + - https + +paths: + /user: + post: + consumes: + - application/json + produces: + - application/jsonÅÅÅ + parameters: + - in: body + name: checkUser + description: Check user + schema: + $ref: '#/definitions/User' + responses: + 201: + description: item created + +definitions: + User: + type: object + required: + - email + properties: + email: + type: string + UserRegister: + allOf: + - $ref: '#/definitions/User' + type: object + required: + - password + properties: + password: + type: string diff --git a/modules/swagger-parser/src/test/resources/allOf-property-relative-file-references/child.yaml b/modules/swagger-parser/src/test/resources/allOf-property-relative-file-references/child.yaml new file mode 100644 index 0000000000..fd62d7a353 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/allOf-property-relative-file-references/child.yaml @@ -0,0 +1,14 @@ +swagger: '2.0' + +info: + version: "0.0.1" + title: Devices API + +basePath: /api/v2/devices + +definitions: + def.def: + type: object + properties: + name: + type: string \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/allOf-property-relative-file-references/parent.yaml b/modules/swagger-parser/src/test/resources/allOf-property-relative-file-references/parent.yaml new file mode 100644 index 0000000000..358c687612 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/allOf-property-relative-file-references/parent.yaml @@ -0,0 +1,16 @@ +swagger: '2.0' + +info: + version: "0.0.1" + title: Devices API + +basePath: /api/v2/devices + +definitions: + test: + type: object + properties: + property: + x-attr: "value" + allOf: + - $ref: "./child.yaml#/definitions/def.def" \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/arrayItemsResolving/ArrayDomain.yaml b/modules/swagger-parser/src/test/resources/arrayItemsResolving/ArrayDomain.yaml new file mode 100644 index 0000000000..0fe25a6475 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/arrayItemsResolving/ArrayDomain.yaml @@ -0,0 +1,38 @@ +info: + description: This is a sample Domain + version: '1.0' + title: Sample Domain + +definitions: + InventoryDataItems: + type: array + items: + type: object + properties: + items: + $ref: '#/definitions/InventoryItem' + manufacturer: + $ref: '#/definitions/Manufacturer' + InventoryItem: + type: object + required: + - id + - name + properties: + id: + type: string + format: uuid + example: d290f1ee-6c54-4b01-90e6-d701748f0851 + name: + type: string + example: Widget Adapter + Manufacturer: + required: + - name + properties: + name: + type: string + example: ACME Corporation + phone: + type: string + example: 408-867-5309 \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/arrayItemsResolving/Swagger.yaml b/modules/swagger-parser/src/test/resources/arrayItemsResolving/Swagger.yaml new file mode 100644 index 0000000000..d0347c9e44 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/arrayItemsResolving/Swagger.yaml @@ -0,0 +1,20 @@ +swagger: '2.0' +info: + description: This is a simple API + version: 1.0.0 + title: Simple Inventory API + +paths: + /inventory: + get: + tags: + - developers + summary: searches inventory + operationId: searchInventory + responses: + 200: + description: search results matching criteria + schema: + type: array + items: + $ref: 'ArrayDomain.yaml#/definitions/InventoryDataItems' diff --git a/modules/swagger-parser/src/test/resources/billion_laughs_snake_yaml.yaml b/modules/swagger-parser/src/test/resources/billion_laughs_snake_yaml.yaml new file mode 100644 index 0000000000..ce3f217d50 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/billion_laughs_snake_yaml.yaml @@ -0,0 +1,2910 @@ +swagger: "2.0" +info: + title: test BLA + version: 1.0.0 +basePath: / +consumes: + - application/json +produces: + - application/json +definitions: + a1: + type: string + enum: &A1 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a2: + type: string + enum: &A2 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a3: + type: string + enum: &A3 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a4: + type: string + enum: &A4 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a5: + type: string + enum: &A5 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a6: + type: string + enum: &A6 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a7: + type: string + enum: &A7 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a8: + type: string + enum: &A8 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a9: + type: string + enum: &A9 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a10: + type: string + enum: &A10 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a11: + type: string + enum: &A11 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a12: + type: string + enum: &A12 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a13: + type: string + enum: &A13 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a14: + type: string + enum: &A14 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a15: + type: string + enum: &A15 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a16: + type: string + enum: &A16 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a17: + type: string + enum: &A17 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a18: + type: string + enum: &A18 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a19: + type: string + enum: &A19 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a20: + type: string + enum: &A20 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a21: + type: string + enum: &A21 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a22: + type: string + enum: &A22 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a23: + type: string + enum: &A23 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a24: + type: string + enum: &A24 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a25: + type: string + enum: &A25 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a26: + type: string + enum: &A26 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a27: + type: string + enum: &A27 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a28: + type: string + enum: &A28 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a29: + type: string + enum: &A29 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a30: + type: string + enum: &A30 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a31: + type: string + enum: &A31 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a32: + type: string + enum: &A32 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a33: + type: string + enum: &A33 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a34: + type: string + enum: &A34 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a35: + type: string + enum: &A35 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a36: + type: string + enum: &A36 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a37: + type: string + enum: &A37 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a38: + type: string + enum: &A38 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a39: + type: string + enum: &A39 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a40: + type: string + enum: &A40 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a41: + type: string + enum: &A41 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a42: + type: string + enum: &A42 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a43: + type: string + enum: &A43 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a44: + type: string + enum: &A44 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a45: + type: string + enum: &A45 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a46: + type: string + enum: &A46 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a47: + type: string + enum: &A47 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a48: + type: string + enum: &A48 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a49: + type: string + enum: &A49 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a50: + type: string + enum: &A50 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a51: + type: string + enum: &A51 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a52: + type: string + enum: &A52 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a53: + type: string + enum: &A53 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a54: + type: string + enum: &A54 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a55: + type: string + enum: &A55 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a56: + type: string + enum: &A56 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a57: + type: string + enum: &A57 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a58: + type: string + enum: &A58 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a59: + type: string + enum: &A59 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a60: + type: string + enum: &A60 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a61: + type: string + enum: &A61 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a62: + type: string + enum: &A62 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a63: + type: string + enum: &A63 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a64: + type: string + enum: &A64 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a65: + type: string + enum: &A65 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a66: + type: string + enum: &A66 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a67: + type: string + enum: &A67 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a68: + type: string + enum: &A68 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a69: + type: string + enum: &A69 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a70: + type: string + enum: &A70 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a71: + type: string + enum: &A71 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a72: + type: string + enum: &A72 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a73: + type: string + enum: &A73 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a74: + type: string + enum: &A74 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a75: + type: string + enum: &A75 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a76: + type: string + enum: &A76 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a77: + type: string + enum: &A77 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a78: + type: string + enum: &A78 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a79: + type: string + enum: &A79 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a80: + type: string + enum: &A80 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a81: + type: string + enum: &A81 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a82: + type: string + enum: &A82 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a83: + type: string + enum: &A83 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a84: + type: string + enum: &A84 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a85: + type: string + enum: &A85 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a86: + type: string + enum: &A86 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a87: + type: string + enum: &A87 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a88: + type: string + enum: &A88 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a89: + type: string + enum: &A89 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a90: + type: string + enum: &A90 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a91: + type: string + enum: &A91 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a92: + type: string + enum: &A92 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a93: + type: string + enum: &A93 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a94: + type: string + enum: &A94 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a95: + type: string + enum: &A95 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a96: + type: string + enum: &A96 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a97: + type: string + enum: &A97 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a98: + type: string + enum: &A98 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a99: + type: string + enum: &A99 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + a100: + type: string + enum: &A100 + - AA1 + - AA2 + - AA3 + - AA4 + - AA5 + - AA6 + - AA7 + - AA8 + - AA9 + - AA10 + - AA11 + - AA12 + - AA13 + - AA14 + - AA15 + - AA16 + - AA17 + - AA18 + - AA19 + - AA20 + c1: + type: object + properties: + a: + type: string + enum: *A1 + c2: + type: object + properties: + a: + type: string + enum: *A2 + c3: + type: object + properties: + a: + type: string + enum: *A3 + c4: + type: object + properties: + a: + type: string + enum: *A4 + c5: + type: object + properties: + a: + type: string + enum: *A5 + c6: + type: object + properties: + a: + type: string + enum: *A6 + c7: + type: object + properties: + a: + type: string + enum: *A7 + c8: + type: object + properties: + a: + type: string + enum: *A8 + c9: + type: object + properties: + a: + type: string + enum: *A9 + c10: + type: object + properties: + a: + type: string + enum: *A10 + c11: + type: object + properties: + a: + type: string + enum: *A11 + c12: + type: object + properties: + a: + type: string + enum: *A12 + c13: + type: object + properties: + a: + type: string + enum: *A13 + c14: + type: object + properties: + a: + type: string + enum: *A14 + c15: + type: object + properties: + a: + type: string + enum: *A15 + c16: + type: object + properties: + a: + type: string + enum: *A16 + c17: + type: object + properties: + a: + type: string + enum: *A17 + c18: + type: object + properties: + a: + type: string + enum: *A18 + c19: + type: object + properties: + a: + type: string + enum: *A19 + c20: + type: object + properties: + a: + type: string + enum: *A20 + c21: + type: object + properties: + a: + type: string + enum: *A21 + c22: + type: object + properties: + a: + type: string + enum: *A22 + c23: + type: object + properties: + a: + type: string + enum: *A23 + c24: + type: object + properties: + a: + type: string + enum: *A24 + c25: + type: object + properties: + a: + type: string + enum: *A25 + c26: + type: object + properties: + a: + type: string + enum: *A26 + c27: + type: object + properties: + a: + type: string + enum: *A27 + c28: + type: object + properties: + a: + type: string + enum: *A28 + c29: + type: object + properties: + a: + type: string + enum: *A29 + c30: + type: object + properties: + a: + type: string + enum: *A30 + c31: + type: object + properties: + a: + type: string + enum: *A31 + c32: + type: object + properties: + a: + type: string + enum: *A32 + c33: + type: object + properties: + a: + type: string + enum: *A33 + c34: + type: object + properties: + a: + type: string + enum: *A34 + c35: + type: object + properties: + a: + type: string + enum: *A35 + c36: + type: object + properties: + a: + type: string + enum: *A36 + c37: + type: object + properties: + a: + type: string + enum: *A37 + c38: + type: object + properties: + a: + type: string + enum: *A38 + c39: + type: object + properties: + a: + type: string + enum: *A39 + c40: + type: object + properties: + a: + type: string + enum: *A40 + c41: + type: object + properties: + a: + type: string + enum: *A41 + c42: + type: object + properties: + a: + type: string + enum: *A42 + c43: + type: object + properties: + a: + type: string + enum: *A43 + c44: + type: object + properties: + a: + type: string + enum: *A44 + c45: + type: object + properties: + a: + type: string + enum: *A45 + c46: + type: object + properties: + a: + type: string + enum: *A46 + c47: + type: object + properties: + a: + type: string + enum: *A47 + c48: + type: object + properties: + a: + type: string + enum: *A48 + c49: + type: object + properties: + a: + type: string + enum: *A49 + c50: + type: object + properties: + a: + type: string + enum: *A50 + c51: + type: object + properties: + a: + type: string + enum: *A51 + c52: + type: object + properties: + a: + type: string + enum: *A52 + c53: + type: object + properties: + a: + type: string + enum: *A53 + c54: + type: object + properties: + a: + type: string + enum: *A54 + c55: + type: object + properties: + a: + type: string + enum: *A55 + c56: + type: object + properties: + a: + type: string + enum: *A56 + c57: + type: object + properties: + a: + type: string + enum: *A57 + c58: + type: object + properties: + a: + type: string + enum: *A58 + c59: + type: object + properties: + a: + type: string + enum: *A59 + c60: + type: object + properties: + a: + type: string + enum: *A60 + c61: + type: object + properties: + a: + type: string + enum: *A61 + c62: + type: object + properties: + a: + type: string + enum: *A62 + c63: + type: object + properties: + a: + type: string + enum: *A63 + c64: + type: object + properties: + a: + type: string + enum: *A64 + c65: + type: object + properties: + a: + type: string + enum: *A65 + c66: + type: object + properties: + a: + type: string + enum: *A66 + c67: + type: object + properties: + a: + type: string + enum: *A67 + c68: + type: object + properties: + a: + type: string + enum: *A68 + c69: + type: object + properties: + a: + type: string + enum: *A69 + c70: + type: object + properties: + a: + type: string + enum: *A70 + c71: + type: object + properties: + a: + type: string + enum: *A71 + c72: + type: object + properties: + a: + type: string + enum: *A72 + c73: + type: object + properties: + a: + type: string + enum: *A73 + c74: + type: object + properties: + a: + type: string + enum: *A74 + c75: + type: object + properties: + a: + type: string + enum: *A75 + c76: + type: object + properties: + a: + type: string + enum: *A76 + c77: + type: object + properties: + a: + type: string + enum: *A77 + c78: + type: object + properties: + a: + type: string + enum: *A78 + c79: + type: object + properties: + a: + type: string + enum: *A79 + c80: + type: object + properties: + a: + type: string + enum: *A80 + c81: + type: object + properties: + a: + type: string + enum: *A81 + c82: + type: object + properties: + a: + type: string + enum: *A82 + c83: + type: object + properties: + a: + type: string + enum: *A83 + c84: + type: object + properties: + a: + type: string + enum: *A84 + c85: + type: object + properties: + a: + type: string + enum: *A85 + c86: + type: object + properties: + a: + type: string + enum: *A86 + c87: + type: object + properties: + a: + type: string + enum: *A87 + c88: + type: object + properties: + a: + type: string + enum: *A88 + c89: + type: object + properties: + a: + type: string + enum: *A89 + c90: + type: object + properties: + a: + type: string + enum: *A90 + c91: + type: object + properties: + a: + type: string + enum: *A91 + c92: + type: object + properties: + a: + type: string + enum: *A92 + c93: + type: object + properties: + a: + type: string + enum: *A93 + c94: + type: object + properties: + a: + type: string + enum: *A94 + c95: + type: object + properties: + a: + type: string + enum: *A95 + c96: + type: object + properties: + a: + type: string + enum: *A96 + c97: + type: object + properties: + a: + type: string + enum: *A97 + c98: + type: object + properties: + a: + type: string + enum: *A98 + c99: + type: object + properties: + a: + type: string + enum: *A99 + c100: + type: object + properties: + a: + type: string + enum: *A100 diff --git a/modules/swagger-parser/src/test/resources/duplicateOperationId.json b/modules/swagger-parser/src/test/resources/duplicateOperationId.json new file mode 100644 index 0000000000..a65301c542 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/duplicateOperationId.json @@ -0,0 +1,34 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.9-abcd", + "title": "Duplicate operationID" + }, + "paths": { + "/pets/{id}": { + "parameters" : [ + { + "in": "path", + "name": "id", + "type": "string" + } + ], + "get": { + "operationId": "getPetsById", + "responses": { + "default": { + "description": "error payload" + } + } + }, + "post": { + "operationId": "getPetsById", + "responses": { + "default": { + "description": "error payload" + } + } + } + } + } +} \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/extensions-responses/extensions.yaml b/modules/swagger-parser/src/test/resources/extensions-responses/extensions.yaml new file mode 100644 index 0000000000..28243db742 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/extensions-responses/extensions.yaml @@ -0,0 +1,164 @@ +swagger: '2.0' +info: + version: '1.0' + title: Extensions everywhere! + x-apis-json: + image: 'https://s3.amazonaws.com/kinlane-productions/api-evangelist/t-shirts/KL_InApiWeTrust-1000.png' + humanURL: 'http://developer.apievangelist.com' + baseURL: 'http://api.apievangelist.com/definitions/Analysis' + tags: + - blog + - industry + - analysis + - new + - API + - Application Programming Interface + properties: + - type: X-signup + url: 'https://apievangelist.3scale.net/' + - type: X-blog + url: 'http://developer.apievangelist.com/blog/' + - type: X-apicommonsmanifest + url: 'https://raw.githubusercontent.com/kinlane/analysis-api/master/api-commons-manifest.json' + contact: + name: Developer + x-contact-extension: Support not available on weekends + license: + name: Apache 2.0 + x-notes: hello + +externalDocs: + description: Learn more about this API + url: http://example.com/api-docs + x-docs-extension: something something + +tags: + - name: pets + x-tag-extension: fooooo + externalDocs: + description: Learn more about pet operations + url: http://example.com/api-docs + x-tag-docs-extension: Yeah + +x-definitions: + DayOfWeek: + type: integer + format: int32 + minimum: 0 + maximum: 6 + +paths: + /post: + post: + consumes: + - application/json + - application/xml + parameters: + - in: body + name: payload + required: true + schema: + type: object + properties: + status: + type: string + required: [status] + responses: + 200: + description: OK + + + x-paths-extension: value + /something: + x-path-item-extension: true + get: + x-version: 1.1 + parameters: + # - name: socialSecurityNumber + # in: query + # description: a social security number + # required: false + # type: string + # x-jwe-encryption: + # algorithm: RSA-OAEP + # encryption: A256GCM + - in: query + name: ids + type: array + items: + type: integer + x-example: 2 + #x-example: [1, 2, 3] + + externalDocs: + description: Learn more about this operation + url: http://example.com/api-docs + x-operation-docs-extension: foo bar + + responses: + x-responses-extension: hello + 200: + description: OK + x-response-extension: OK?! + headers: + X-Rate-Limit: + type: integer + x-example: 200 + +definitions: + String: + type: string + x-nullable: true + + ArrayOfInt: + x-nullable: true + type: array + items: + type: integer + x-nullable: true + + User: + type: object + xml: + name: USER + x-xml-extension: something + + Dictionary: + type: object + additionalProperties: + type: integer + x-extension: Foo bar + externalDocs: + description: Learn more about dictionaries + url: http://api.example.com/docs + x-extension: Yeah + +securityDefinitions: + BasicAuth: + type: basic + description: Basic authentication using login and password + APIKeyHeader: + type: apiKey + in: header + name: X-API-Key + APIKeyQueryParam: + type: apiKey + in: query + name: api_key + OAuth2Implicit: + type: oauth2 + authorizationUrl: http://swagger.io/api/oauth/dialog + flow: implicit + x-auth-extension: hello + scopes: + write:pets: modify pets in your account + read:pets: read your pets + x-scope-extension: hello + +security: + - BasicAuth: [] + - APIKeyHeader: [] + APIKeyQueryParam: [] + - OAuth2Implicit: + - read:pets + - write:pets \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/flatten.json b/modules/swagger-parser/src/test/resources/flatten.json new file mode 100644 index 0000000000..dbbb4ddcd3 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/flatten.json @@ -0,0 +1,26 @@ +{ + "swagger" : "2.0", + "definitions" : { + "User" : { + "required" : [ "address" ], + "properties" : { + "name" : { + "type" : "string" + }, + "address" : { + "type" : "object", + "description" : "description", + "properties" : { + "city" : { + "type" : "string" + }, + "street" : { + "type" : "string" + } + } + } + }, + "description" : "a common user" + } + } +} diff --git a/modules/swagger-parser/src/test/resources/folder/domains/Test/api.test.com/v2.yaml b/modules/swagger-parser/src/test/resources/folder/domains/Test/api.test.com/v2.yaml new file mode 100644 index 0000000000..8c7bce42fe --- /dev/null +++ b/modules/swagger-parser/src/test/resources/folder/domains/Test/api.test.com/v2.yaml @@ -0,0 +1,47 @@ +### Domains, a place to put your reusable components + +responses: + GeneralError: + description: Occurs when something goes wrong + schema: + $ref: 'v2.yaml#/definitions/Test.Common.ErrorResponseMessage' + + +definitions: + + Test.Common.DocUrl: + type: string + format: uri + description: The documentation related to this operation. + example: 'https://api-docs.test.com/#operation/EnableChannelCatalog' + + + Test.Common.ErrorResponseMessage: + type: object + required: + - errors + properties: + errors: + type: array + uniqueItems: false + items: + $ref: 'v2.yaml#/definitions/Test.Common.UserErrorMessage' + + Test.Common.UserErrorMessage: + type: object + required: + - code + - message + properties: + docUrl: + $ref: 'v2.yaml#/definitions/Test.Common.DocUrl' + code: + type: string + description: the error code. The error code can be a pattern containing the argument's name + example: Here goes an example text + message: + type: string + description: The error message + example: | + There is already an importation in progress: 12345-XYZBN-00122-44444 + diff --git a/modules/swagger-parser/src/test/resources/issue-111.yaml b/modules/swagger-parser/src/test/resources/issue-111.yaml new file mode 100644 index 0000000000..8923e1a5f5 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-111.yaml @@ -0,0 +1,34 @@ +swagger: '2.0' +info: + version: '0.0.2' + title: 'Test' + description: + This is the test API documentation +produces: + - application/json +paths: + + '/test': + get: + summary: test + description: Test Endpoint + produces: + - application/json + operationId: getTest + parameters: + - name: query + in: query + description: Query That Tests + required: true + type: string + responses: + 200: + description: TestResult + schema: + $ref: '#/definitions/Filter' +definitions: + Filter: + description: Contains the the information for filter presentation + properties: + hints: + type: array \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-1146.yaml b/modules/swagger-parser/src/test/resources/issue-1146.yaml new file mode 100644 index 0000000000..ba766abe9b --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-1146.yaml @@ -0,0 +1,39 @@ +swagger: "2.0" +info: + description: "Test" + version: "1.0.0" + title: "Swagger v2 example" + termsOfService: "http://swagger.io/terms/" + contact: + email: "apiteam@swagger.io" + license: + name: "Apache 2.0" + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +host: "petstore.swagger.io" +basePath: "/v2" +schemes: + - "https" + - "http" +paths: + /brands/{brand-id}/stations: + get: + tags: + - Brand + summary: Returns a list of the brand's stations + operationId: getBrandStations + parameters: + - $ref: "#/parameters/brand-id" + - in: query + name: available-to-client-only + required: true + type: boolean + responses: + 200: + description: OK +parameters: + brand-id: + in: path + description: this is a test + name: brand-id + required: true + type: string \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-1249.json b/modules/swagger-parser/src/test/resources/issue-1249.json new file mode 100644 index 0000000000..77d4c122df --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-1249.json @@ -0,0 +1,35 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "Example API" + }, + "host": "example.com", + "basePath": "/", + "paths": { + "/api/admin/{account}/users/{user}": { + "post": { + "operationId": "addUser", + "parameters": [ + { + "name": "account", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "user", + "in": "path", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + } +} \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-1432.yaml b/modules/swagger-parser/src/test/resources/issue-1432.yaml new file mode 100644 index 0000000000..e2a235de9d --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-1432.yaml @@ -0,0 +1,12 @@ +swagger: "2.0" +info: + description: some description + title: data + version: "1" +paths: + /tickets: + get: + responses: + 200: + title: abc + description: data \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-1541/main.yaml b/modules/swagger-parser/src/test/resources/issue-1541/main.yaml new file mode 100644 index 0000000000..6a8041d124 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-1541/main.yaml @@ -0,0 +1,38 @@ +swagger: '2.0' +info: + title: 'Issue 1541' + version: '1.0' +consumes: + - application/json +paths: + /inline_response: + get: + responses: + '200': + description: ok + schema: + type: object + additionalProperties: false + properties: + a: + type: number + /ref_response: + get: + responses: + '200': + description: ok + schema: + $ref: '#/definitions/ref_response_object' +definitions: + ref_response_object: + type: object + additionalProperties: false + properties: + a: + type: number + b: + type: object + additionalProperties: false + properties: + c: { type: number } + d: { type: string } diff --git a/modules/swagger-parser/src/test/resources/issue-901/ref.yaml b/modules/swagger-parser/src/test/resources/issue-901/ref.yaml new file mode 100644 index 0000000000..094abb907d --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-901/ref.yaml @@ -0,0 +1,33 @@ +### Domains, a place to put your reusable components + + +info: + title: "aa" + description: "swos552" + version: '1.0.0' + +definitions: + Test.Definition: + type: object + properties: + stuff: + type: array + items: + $ref: '#/definitions/TESTTHING' + TESTTHING: + type: object + properties: + prop: + type: string +pathitems: + path-test: + put: + description: test ref + operationId: test + produces: + - application/json + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/Test.Definition' \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-901/ref2.yaml b/modules/swagger-parser/src/test/resources/issue-901/ref2.yaml new file mode 100644 index 0000000000..ac89aacfca --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-901/ref2.yaml @@ -0,0 +1,35 @@ +### Domains, a place to put your reusable components + + +info: + title: "aa" + description: "swos55" + version: '1.0.0' + +definitions: + Test.Definition: + type: object + properties: + stuff: + type: array + items: + $ref: '#/definitions/TEST.THING.OUT.Stuff' + TEST.THING.OUT.Stuff: + type: object + properties: + prop: + type: string +pathitems: + path-test: + put: + description: test ref + operationId: test + produces: + - application/json + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/Test.Definition' + + diff --git a/modules/swagger-parser/src/test/resources/issue-901/spec.yaml b/modules/swagger-parser/src/test/resources/issue-901/spec.yaml new file mode 100644 index 0000000000..ebaee269a7 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-901/spec.yaml @@ -0,0 +1,9 @@ +swagger: '2.0' +info: + version: '1.01' + title: testswos55 + description: testswos55 +basePath: /test +paths: + '/test': + $ref: 'ref.yaml/#/pathitems/path-test' \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-901/spec2.yaml b/modules/swagger-parser/src/test/resources/issue-901/spec2.yaml new file mode 100644 index 0000000000..2932a7e1db --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-901/spec2.yaml @@ -0,0 +1,9 @@ +swagger: '2.0' +info: + version: '1.01' + title: testswos55 + description: testswos55 +basePath: /test +paths: + '/test': + $ref: 'ref2.yaml/#/pathitems/path-test' \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-913/BO/Common/BasicComponents.json b/modules/swagger-parser/src/test/resources/issue-913/BO/Common/BasicComponents.json new file mode 100644 index 0000000000..eebd403570 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-913/BO/Common/BasicComponents.json @@ -0,0 +1,12 @@ +{ + "properties": { + "indicatorType": { + "type": "object", + "properties": { + "indicator": { + "type": "boolean" + } + } + } + } +} \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-913/BO/Resource/ApiSpecificationBO.json b/modules/swagger-parser/src/test/resources/issue-913/BO/Resource/ApiSpecificationBO.json new file mode 100644 index 0000000000..54b85dfaa9 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-913/BO/Resource/ApiSpecificationBO.json @@ -0,0 +1,12 @@ +{ + "properties": { + "details": { + "type": "object", + "properties": { + "confirmationCodeRequired": { + "$ref": "../Common/BasicComponents.json#/properties/indicatorType" + } + } + } + } +} diff --git a/modules/swagger-parser/src/test/resources/issue-913/BS/ApiSpecification.yaml b/modules/swagger-parser/src/test/resources/issue-913/BS/ApiSpecification.yaml new file mode 100644 index 0000000000..c3314bd4e9 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-913/BS/ApiSpecification.yaml @@ -0,0 +1,60 @@ +{ + "swagger": "2.0", + "info": { + "title": "Service Model", + "version": "2.4.0" + }, + "host": "api.com", + "basePath": "/{access}/accessProfileAPI", + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/networkAccessProfile": { + "get": { + "tags": [ + "AccessProfile" + ], + "operationId": "getList", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "id", + "in": "query", + "type": "string" + } + ], + "responses": { + "200": { + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/accessProfileBO" + } + } + } + } + } + } + }, + "definitions": { + "accessProfileBO": { + "$ref": "../BO/Resource/ApiSpecificationBO.json" + } + }, + "parameters": { + "fields": { + "name": "fields", + "in": "query", + "type": "string" + } + } +} \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-927/issue-927.yaml b/modules/swagger-parser/src/test/resources/issue-927/issue-927.yaml new file mode 100644 index 0000000000..199b86b14f --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-927/issue-927.yaml @@ -0,0 +1,14 @@ +# API definition + +swagger: '2.0' +info: + version: '1.0.0' + title: Discriminator and Resolved YAML +paths: + /pet: + get: + responses: + 200: + description: A single pet + schema: + $ref: "remote.yaml/#/definitions/Pet" \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-927/remote.yaml b/modules/swagger-parser/src/test/resources/issue-927/remote.yaml new file mode 100644 index 0000000000..6836869519 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-927/remote.yaml @@ -0,0 +1,47 @@ +# Domain + +definitions: + Pet: + type: object + discriminator: petType + properties: + name: + type: string + petType: + type: string + enum: [Cat, Dog] + required: + - name + - petType + + Cat: ## "Cat" will be used as the discriminator value + description: A representation of a cat + allOf: + - $ref: '#/definitions/Pet' + - type: object + properties: + huntingSkill: + type: string + description: The measured skill for hunting + enum: + - clueless + - lazy + - adventurous + - aggressive + required: + - huntingSkill + + Dog: ## "Dog" will be used as the discriminator value + description: A representation of a dog + allOf: + - $ref: '#/definitions/Pet' + - type: object + properties: + packSize: + type: integer + format: int32 + description: the size of the pack the dog is from + default: 0 + minimum: 0 + required: + - packSize \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-995/digitalExp-CommonDefinitionDomain.yaml b/modules/swagger-parser/src/test/resources/issue-995/digitalExp-CommonDefinitionDomain.yaml new file mode 100644 index 0000000000..395a117838 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-995/digitalExp-CommonDefinitionDomain.yaml @@ -0,0 +1,112 @@ +info: + description: "This is a common Domain" + version: "1.0" + title: Common Domain + +responses: + + 400-BadRequest: + x-dox-Since: v10.2 + description: Bad Request + schema: + $ref: "#/definitions/ErrorResponse" + +definitions: + ErrorResponse: + x-dox-Since: v10.2 + type: object + description: | + Response containing descriptive error text, error code + required: + - code + - message + properties: + code: + description: | + The code associated with the error + readOnly: true + type: integer + message: + description: | + The message associated with the error + readOnly: true + type: string + + ChannelType: + type: string + enum: + - selfService + - retail + - callCenter + + Product: + x-dox-Since: '10.2' + description: | + The respesentation of the product + type: object + discriminator: productType + required: + - productType + x-dox-discriminator-name: NAME + x-dox-discriminator-type: + - MobileProduct + - InternetProduct + - FixedVoiceProduct + properties: + id: + type: string + status: + type: string + name: + type: string + productType: + $ref: '#/definitions/ProductType' + + ProductType: + x-dox-Since: '10.2' + description: | + The different type of products + enum: + - MobileProduct + - FixedVoiceProduct + - InternetProduct + + MobileProduct: + x-dox-Since: 10.2 + description: | + The instance of the mobile product being added. + type: object + allOf: + - $ref: "#/definitions/Product" + - type: object + properties: + sim: + type: string + phoneNumber: + type: string + + FixedVoiceProduct: + x-dox-Since: 10.2 + description: | + The instance of the fixed voice product being added. + type: object + allOf: + - $ref: "#/definitions/Product" + - type: object + properties: + phoneNumber: + type: string + + InternetProduct: + x-dox-Since: 10.2 + description: | + The instance of the internet product being added. + type: object + allOf: + - $ref: "#/definitions/Product" + - type: object + properties: + userName: + type: string + speed: + type: string \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-995/digitalExp-Product-Unresolved.yaml b/modules/swagger-parser/src/test/resources/issue-995/digitalExp-Product-Unresolved.yaml new file mode 100644 index 0000000000..79ced47d3b --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-995/digitalExp-Product-Unresolved.yaml @@ -0,0 +1,32 @@ +swagger: '2.0' +info: + version: '1.0' + title: 'Product Operation' + description: 'Product Operation' + + +basePath: /commerce + +consumes: + - application/json +produces: + - application/json + +paths: + + /v1/product/{productId}: + get: + summary: Get the product + description: Get the product + parameters: + - name: productId + in: path + required: true + type: string + responses: + 200: + description: OK + schema: + $ref: 'digitalExp-CommonDefinitionDomain.yaml#/definitions/Product' + 400: + $ref: 'digitalExp-CommonDefinitionDomain.yaml#/responses/400-BadRequest' \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue-swg-14378.yaml b/modules/swagger-parser/src/test/resources/issue-swg-14378.yaml new file mode 100644 index 0000000000..e82ab1d2f8 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue-swg-14378.yaml @@ -0,0 +1,10 @@ +swagger: "2.0" +info: + description: some description + title: data +paths: + /tickets: + get: + responses: + 200: + description: data \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue1143.json b/modules/swagger-parser/src/test/resources/issue1143.json new file mode 100644 index 0000000000..67984ec0d0 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue1143.json @@ -0,0 +1,1404 @@ +{ + "swagger": "2.0", + "info": { + "title": "RedisManagementClient", + "description": "REST API for Azure Redis Cache Service.", + "version": "2018-03-01" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "flow": "implicit", + "description": "Azure Active Directory OAuth2 Flow.", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "paths": { + "/providers/Microsoft.Cache/operations": { + "get": { + "tags": [ + "Operations" + ], + "description": "Lists all of the available REST API operations of the Microsoft.Cache provider.", + "operationId": "Operations_List", + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Success. The response describes the list of operations.", + "schema": { + "$ref": "#/definitions/OperationListResult" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/providers/Microsoft.Cache/CheckNameAvailability": { + "post": { + "tags": [ + "Redis" + ], + "operationId": "Redis_CheckNameAvailability", + "x-ms-examples": { + "RedisCacheList": { + "$ref": "./examples/RedisCacheCheckNameAvailability.json" + } + }, + "description": "Checks that the redis cache name is valid and is not already in use.", + "parameters": [ + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CheckNameAvailabilityParameters" + }, + "description": "Parameters supplied to the CheckNameAvailability Redis operation. The only supported resource type is 'Microsoft.Cache/redis'" + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Name is available" + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}/listUpgradeNotifications": { + "get": { + "tags": [ + "Redis" + ], + "operationId": "Redis_ListUpgradeNotifications", + "x-ms-examples": { + "RedisCacheGet": { + "$ref": "./examples/RedisCacheListUpgradeNotifications.json" + } + }, + "description": "Gets any upgrade notifications for a Redis cache.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "name": "history", + "in": "query", + "required": true, + "type": "number", + "format": "double", + "description": "how many minutes in past to look for upgrade notifications" + } + ], + "responses": { + "200": { + "description": "All upgrade notifications in given time range", + "schema": { + "$ref": "#/definitions/NotificationListResponse" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}": { + "put": { + "tags": [ + "Redis" + ], + "operationId": "Redis_Create", + "x-ms-examples": { + "RedisCacheCreate": { + "$ref": "./examples/RedisCacheCreate.json" + } + }, + "description": "Create or replace (overwrite/recreate, with potential downtime) an existing Redis cache.", + "x-ms-long-running-operation": true, + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RedisCreateParameters" + }, + "description": "Parameters supplied to the Create Redis operation." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "201": { + "description": "The new redis cache was successfully created. Check provisioningState to see detailed status.", + "schema": { + "$ref": "#/definitions/RedisResource" + } + }, + "200": { + "description": "The existing redis cache was successfully updated. Check provisioningState to see detailed status.", + "schema": { + "$ref": "#/definitions/RedisResource" + } + } + } + }, + "patch": { + "tags": [ + "Redis" + ], + "operationId": "Redis_Update", + "x-ms-examples": { + "RedisCacheUpdate": { + "$ref": "./examples/RedisCacheUpdate.json" + } + }, + "description": "Update an existing Redis cache.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RedisUpdateParameters" + }, + "description": "Parameters supplied to the Update Redis operation." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "The existing redis cache was successfully updated. Check provisioningState to see detailed status.", + "schema": { + "$ref": "#/definitions/RedisResource" + } + } + } + }, + "delete": { + "tags": [ + "Redis" + ], + "operationId": "Redis_Delete", + "x-ms-examples": { + "RedisCacheDelete": { + "$ref": "./examples/RedisCacheDelete.json" + } + }, + "description": "Deletes a Redis cache.", + "x-ms-long-running-operation": true, + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "The redis cache was successfully deleted." + }, + "202": { + "description": "The redis cache 'delete' operation was successfully enqueued; follow the Location header to poll for final outcome." + }, + "204": { + "description": "The redis cache was successfully deleted." + } + } + }, + "get": { + "tags": [ + "Redis" + ], + "operationId": "Redis_Get", + "x-ms-examples": { + "RedisCacheGet": { + "$ref": "./examples/RedisCacheGet.json" + } + }, + "description": "Gets a Redis cache (resource description).", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "The redis cache was successfully found.", + "schema": { + "$ref": "#/definitions/RedisResource" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis": { + "get": { + "tags": [ + "Redis" + ], + "operationId": "Redis_ListByResourceGroup", + "x-ms-examples": { + "RedisCacheListByResourceGroup": { + "$ref": "./examples/RedisCacheListByResourceGroup.json" + } + }, + "description": "Lists all Redis caches in a resource group.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/RedisListResult" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/providers/Microsoft.Cache/Redis": { + "get": { + "tags": [ + "Redis" + ], + "operationId": "Redis_List", + "x-ms-examples": { + "RedisCacheList": { + "$ref": "./examples/RedisCacheList.json" + } + }, + "description": "Gets all Redis caches in the specified subscription.", + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/RedisListResult" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}/listKeys": { + "post": { + "tags": [ + "Redis" + ], + "operationId": "Redis_ListKeys", + "x-ms-examples": { + "RedisCacheListKeys": { + "$ref": "./examples/RedisCacheListKeys.json" + } + }, + "description": "Retrieve a Redis cache's access keys. This operation requires write permission to the cache resource.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Lists the keys for the specified Redis cache.", + "schema": { + "$ref": "#/definitions/RedisAccessKeys" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}/regenerateKey": { + "post": { + "tags": [ + "Redis" + ], + "operationId": "Redis_RegenerateKey", + "x-ms-examples": { + "RedisCacheRegenerateKey": { + "$ref": "./examples/RedisCacheRegenerateKey.json" + } + }, + "description": "Regenerate Redis cache's access keys. This operation requires write permission to the cache resource.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RedisRegenerateKeyParameters" + }, + "description": "Specifies which key to regenerate." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Lists the regenerated keys for Redis Cache", + "schema": { + "$ref": "#/definitions/RedisAccessKeys" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}/forceReboot": { + "post": { + "tags": [ + "Redis" + ], + "operationId": "Redis_ForceReboot", + "x-ms-examples": { + "RedisCacheForceReboot": { + "$ref": "./examples/RedisCacheForceReboot.json" + } + }, + "description": "Reboot specified Redis node(s). This operation requires write permission to the cache resource. There can be potential data loss.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RedisRebootParameters" + }, + "description": "Specifies which Redis node(s) to reboot." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Reboot operation successfully enqueued", + "schema": { + "$ref": "#/definitions/RedisForceRebootResponse" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}/import": { + "post": { + "tags": [ + "Redis" + ], + "operationId": "Redis_ImportData", + "x-ms-examples": { + "RedisCacheImport": { + "$ref": "./examples/RedisCacheImport.json" + } + }, + "x-ms-long-running-operation": true, + "description": "Import data into Redis cache.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ImportRDBParameters" + }, + "description": "Parameters for Redis import operation." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "202": { + "description": "Import operation successfully enqueued; follow the Location header to poll for final outcome." + }, + "200": { + "description": "Import operation succeeded." + }, + "204": { + "description": "Import operation succeeded." + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}/export": { + "post": { + "tags": [ + "Redis" + ], + "operationId": "Redis_ExportData", + "x-ms-examples": { + "RedisCacheExport": { + "$ref": "./examples/RedisCacheExport.json" + } + }, + "x-ms-long-running-operation": true, + "description": "Export data from the redis cache to blobs in a container.", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ExportRDBParameters" + }, + "description": "Parameters for Redis export operation." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "202": { + "description": "Export operation successfully enqueued; follow the Location header to poll for final outcome." + }, + "200": { + "description": "Export operation succeeded." + }, + "204": { + "description": "Export operation succeeded." + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{cacheName}/firewallRules": { + "get": { + "tags": [ + "Redis", + "FirewallRules" + ], + "operationId": "FirewallRules_ListByRedisResource", + "description": "Gets all firewall rules in the specified redis cache.", + "x-ms-examples": { + "RedisCacheFirewallRulesList": { + "$ref": "./examples/RedisCacheFirewallRulesList.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "cacheName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + } + ], + "responses": { + "200": { + "description": "Successfully got the current rules", + "schema": { + "$ref": "#/definitions/RedisFirewallRuleListResult" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{cacheName}/firewallRules/{ruleName}": { + "put": { + "tags": [ + "Redis", + "FirewallRules" + ], + "operationId": "FirewallRules_CreateOrUpdate", + "description": "Create or update a redis cache firewall rule", + "x-ms-examples": { + "RedisCacheFirewallRuleCreate": { + "$ref": "./examples/RedisCacheFirewallRuleCreate.json" + } + }, + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "cacheName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "name": "ruleName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the firewall rule." + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RedisFirewallRuleCreateParameters" + }, + "description": "Parameters supplied to the create or update redis firewall rule operation." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Resource was successfully updated", + "schema": { + "$ref": "#/definitions/RedisFirewallRule" + } + }, + "201": { + "description": "Resource was successfully created", + "schema": { + "$ref": "#/definitions/RedisFirewallRule" + } + } + } + }, + "get": { + "tags": [ + "Redis", + "FirewallRules" + ], + "operationId": "FirewallRules_Get", + "description": "Gets a single firewall rule in a specified redis cache.", + "x-ms-examples": { + "RedisCacheFirewallRuleGet": { + "$ref": "./examples/RedisCacheFirewallRuleGet.json" + } + }, + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "cacheName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "name": "ruleName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the firewall rule." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Successfully found the rule", + "schema": { + "$ref": "#/definitions/RedisFirewallRule" + } + } + } + }, + "delete": { + "tags": [ + "Redis", + "FirewallRules" + ], + "operationId": "FirewallRules_Delete", + "description": "Deletes a single firewall rule in a specified redis cache.", + "x-ms-examples": { + "RedisCacheFirewallRuleDelete": { + "$ref": "./examples/RedisCacheFirewallRuleDelete.json" + } + }, + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "cacheName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "name": "ruleName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the firewall rule." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Successfully deleted the rule" + }, + "204": { + "description": "Successfully deleted the rule" + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{cacheName}/patchSchedules": { + "get": { + "tags": [ + "Redis", + "PatchSchedules" + ], + "operationId": "PatchSchedules_ListByRedisResource", + "description": "Gets all patch schedules in the specified redis cache (there is only one).", + "x-ms-examples": { + "RedisCachePatchSchedulesList": { + "$ref": "./examples/RedisCachePatchSchedulesList.json" + } + }, + "parameters": [ + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + }, + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "cacheName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + } + ], + "responses": { + "200": { + "description": "Successfully got the current patch schedules", + "schema": { + "$ref": "#/definitions/RedisPatchScheduleListResult" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}/patchSchedules/{default}": { + "put": { + "tags": [ + "Redis", + "PatchSchedules" + ], + "operationId": "PatchSchedules_CreateOrUpdate", + "x-ms-examples": { + "RedisCachePatchSchedulesCreateOrUpdate": { + "$ref": "./examples/RedisCachePatchSchedulesCreateOrUpdate.json" + } + }, + "description": "Create or replace the patching schedule for Redis cache (requires Premium SKU).", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "name": "default", + "in": "path", + "required": true, + "type": "string", + "description": "Default string modeled as parameter for auto generation to work correctly.", + "enum": [ + "default" + ], + "x-ms-enum": { + "name": "defaultName", + "modelAsString": true + } + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RedisPatchSchedule" + }, + "description": "Parameters to set the patching schedule for Redis cache." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "The patch schedule was successfully updated.", + "schema": { + "$ref": "#/definitions/RedisPatchSchedule" + } + }, + "201": { + "description": "The patch schedule was successfully created.", + "schema": { + "$ref": "#/definitions/RedisPatchSchedule" + } + } + } + }, + "delete": { + "tags": [ + "Redis", + "PatchSchedules" + ], + "operationId": "PatchSchedules_Delete", + "x-ms-examples": { + "RedisCachePatchSchedulesDelete": { + "$ref": "./examples/RedisCachePatchSchedulesDelete.json" + } + }, + "description": "Deletes the patching schedule of a redis cache (requires Premium SKU).", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the redis cache." + }, + { + "name": "default", + "in": "path", + "required": true, + "type": "string", + "description": "Default string modeled as parameter for auto generation to work correctly.", + "enum": [ + "default" + ], + "x-ms-enum": { + "name": "defaultName", + "modelAsString": true + } + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Success." + }, + "204": { + "description": "Success." + } + } + }, + "get": { + "tags": [ + "Redis", + "PatchSchedules" + ], + "operationId": "PatchSchedules_Get", + "x-ms-examples": { + "RedisCachePatchSchedulesGet": { + "$ref": "./examples/RedisCachePatchSchedulesGet.json" + } + }, + "description": "Gets the patching schedule of a redis cache (requires Premium SKU).", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the redis cache." + }, + { + "name": "default", + "in": "path", + "required": true, + "type": "string", + "description": "Default string modeled as parameter for auto generation to work correctly.", + "enum": [ + "default" + ], + "x-ms-enum": { + "name": "defaultName", + "modelAsString": true + } + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Response of get patch schedules.", + "schema": { + "$ref": "#/definitions/RedisPatchSchedule" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}/linkedServers/{linkedServerName}": { + "put": { + "tags": [ + "Redis" + ], + "operationId": "LinkedServer_Create", + "x-ms-long-running-operation": true, + "x-ms-examples": { + "LinkedServer_Create": { + "$ref": "./examples/RedisCacheLinkedServer_Create.json" + } + }, + "description": "Adds a linked server to the Redis cache (requires Premium SKU).", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the Redis cache." + }, + { + "name": "linkedServerName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the linked server that is being added to the Redis cache." + }, + { + "name": "parameters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RedisLinkedServerCreateParameters" + }, + "description": "Parameters supplied to the Create Linked server operation." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "The linked server was successfully added.", + "schema": { + "$ref": "#/definitions/RedisLinkedServerWithProperties" + } + }, + "201": { + "description": "The linked server was successfully added.", + "schema": { + "$ref": "#/definitions/RedisLinkedServerWithProperties" + } + } + } + }, + "delete": { + "tags": [ + "Redis" + ], + "operationId": "LinkedServer_Delete", + "x-ms-examples": { + "LinkedServerDelete": { + "$ref": "./examples/RedisCacheLinkedServer_Delete.json" + } + }, + "description": "Deletes the linked server from a redis cache (requires Premium SKU).", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the redis cache." + }, + { + "name": "linkedServerName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the linked server that is being added to the Redis cache." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Linked server was successfully deleted." + } + } + }, + "get": { + "tags": [ + "Redis" + ], + "operationId": "LinkedServer_Get", + "x-ms-examples": { + "LinkedServer_Get": { + "$ref": "./examples/RedisCacheLinkedServer_Get.json" + } + }, + "description": "Gets the detailed information about a linked server of a redis cache (requires Premium SKU).", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the redis cache." + }, + { + "name": "linkedServerName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the linked server." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Response of get linked server.", + "schema": { + "$ref": "#/definitions/RedisLinkedServerWithProperties" + } + } + } + } + }, + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Cache/Redis/{name}/linkedServers": { + "get": { + "tags": [ + "Redis" + ], + "operationId": "LinkedServer_List", + "x-ms-examples": { + "LinkedServer_List": { + "$ref": "./examples/RedisCacheLinkedServer_List.json" + } + }, + "description": "Gets the list of linked servers associated with this redis cache (requires Premium SKU).", + "parameters": [ + { + "name": "resourceGroupName", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the resource group." + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "The name of the redis cache." + }, + { + "$ref": "#/parameters/ApiVersionParameter" + }, + { + "$ref": "#/parameters/SubscriptionIdParameter" + } + ], + "responses": { + "200": { + "description": "Response of get linked servers.", + "schema": { + "$ref": "#/definitions/RedisLinkedServerWithPropertiesList" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + } + }, + "definitions": { + "identificacion_usuario_aplicacion": { + "allOf": [ + { + "$ref": "#/definitions/identificacion_usuario" + } + ], + "properties": { + "aplicacion": { + "type": "string", + "description": "some description", + "example": "some example value" + } + } + }, + "RedisResource": { + "properties": { + "properties": { + "x-ms-client-flatten": true, + "$ref": "#/definitions/RedisProperties", + "description": "Redis cache properties." + }, + "zones": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of availability zones denoting where the resource needs to come from." + } + }, + "required": [ + "properties" + ], + "allOf": [ + { + "$ref": "#/definitions/TrackedResource" + } + ], + "description": "A single Redis item in List or Get Operation." + } + }, + "parameters": { + "SubscriptionIdParameter": { + "name": "subscriptionId", + "in": "path", + "required": true, + "type": "string", + "description": "Gets subscription credentials which uniquely identify the Microsoft Azure subscription. The subscription ID forms part of the URI for every service call." + }, + "ApiVersionParameter": { + "name": "api-version", + "in": "query", + "required": true, + "type": "string", + "description": "Client Api Version." + } + } +} \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue1169-noSplit.yaml b/modules/swagger-parser/src/test/resources/issue1169-noSplit.yaml new file mode 100644 index 0000000000..5cfeaa5aac --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue1169-noSplit.yaml @@ -0,0 +1,32 @@ +swagger: '2.0' +paths: + '/path_{p0}/{p1}{p2}/another-{p3}': + put: + consumes: + - application/json + produces: + - application/json + parameters: + - name: p0 + in: path + required: true + type: string + - name: p1 + in: path + required: true + type: string + - name: p2 + in: path + required: true + type: string + - name: p3 + in: path + required: true + type: string + responses: + '200': + description: sample desc +info: + version: 1.0.0 + title: Sample title + \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue1169.yaml b/modules/swagger-parser/src/test/resources/issue1169.yaml new file mode 100644 index 0000000000..7f2766b967 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue1169.yaml @@ -0,0 +1,179 @@ +swagger: "2.0" +info: + description: "hhhhhhhhhh\r\niiiiiiii\r\njjjjjjjjjj" + version: "1.0" + title: "test1" +host: "127.0.0.1:5555" +basePath: "/rad/P1:rad1" +schemes: + - "http" +consumes: + - "application/json" + - "application/xml" + - "text/xml" + - "text/html" + - "application/x-www-form-urlencoded" + - "application/vnd.api+json" +produces: + - "application/json" + - "application/xml" + - "text/xml" + - "text/html" + - "application/x-www-form-urlencoded" + - "application/vnd.api+json" +paths: + /test/{id}-{name}: + options: + operationId: "testidname_GET_1" + parameters: + - name: "id" + in: "path" + required: true + type: "string" + - name: "name" + in: "path" + required: true + type: "string" + - name: "age" + in: "header" + required: false + type: "number" + format: "float" + - name: "sex" + in: "formData" + required: false + type: "boolean" + - name: "date" + in: "header" + required: false + type: "string" + format: "date" + responses: + 200: + description: "OK" + schema: + $ref: "#/definitions/testidname_GET_response" + 401: + description: "Access Denied" + head: + operationId: "testidname_POST_1" + consumes: + - "application/json" + - "application/vnd.api+json" + - "application/x-www-form-urlencoded" + - "application/xml" + - "text/html" + - "text/xml" + produces: + - "application/json" + - "application/vnd.api+json" + - "application/x-www-form-urlencoded" + - "application/xml" + - "text/html" + - "text/xml" + parameters: + - name: "id" + in: "path" + required: true + type: "string" + - name: "name" + in: "path" + required: true + type: "string" + - name: "age" + in: "header" + required: false + type: "number" + format: "float" + - name: "sex" + in: "query" + required: false + type: "boolean" + - name: "date" + in: "query" + required: false + type: "string" + format: "date" + - in: "body" + name: "R1Reffered" + required: false + schema: + $ref: "#/definitions/R1" + responses: + 200: + description: "OK" + schema: + $ref: "#/definitions/testidname_POST_response" + 401: + description: "Access Denied" + /test: + delete: + operationId: "test_DELETE_2" + consumes: + - "application/json" + produces: + - "application/vnd.api+json" + parameters: + - name: "id" + in: "header" + required: true + type: "string" + - name: "name" + in: "header" + required: true + type: "string" + - name: "age" + in: "header" + required: false + type: "number" + format: "float" + - name: "sex" + in: "header" + required: false + type: "boolean" + - name: "date" + in: "header" + required: false + type: "string" + format: "date" + - in: "body" + name: "R1Reffered" + required: false + schema: + $ref: "#/definitions/R1" + responses: + 200: + description: "OK" + schema: + $ref: "#/definitions/test_DELETE_response" + 401: + description: "Access Denied" +definitions: + out: + properties: + o1: + type: "string" + testidname_GET_response: + required: + - "out" + properties: + out: + $ref: "#/definitions/out" + test_DELETE_response: + required: + - "out" + properties: + out: + $ref: "#/definitions/out" + testidname_POST_response: + required: + - "out" + properties: + out: + $ref: "#/definitions/out" + R1: + required: + - "id" + properties: + id: + type: "string" diff --git a/modules/swagger-parser/src/test/resources/issue1204.yaml b/modules/swagger-parser/src/test/resources/issue1204.yaml new file mode 100644 index 0000000000..711905cedc --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue1204.yaml @@ -0,0 +1,3100 @@ +swagger: '2.0' +schemes: + - https +host: api.twitter.com +basePath: /1.1 +info: + contact: + email: support@twitter.com + name: Twitter support + url: 'https://dev.twitter.com' + x-twitter: twitter + title: Twitter + version: '1.1' + x-apisguru-categories: + - social + x-logo: + url: 'https://api.apis.guru/v2/cache/logo/https_twitter.com_twitter_profile_image.png' + x-origin: + - converter: + url: 'https://github.com/lucybot/api-spec-converter' + version: 2.6.0 + format: wadl + url: 'http://api.apigee.com/v1/consoles/twitter/apidescription?format=wadl' + - format: swagger + url: 'https://raw.githubusercontent.com/APIs-guru/unofficial_openapi_specs/master/twitter.com/1.1/twitter.json' + version: '2.0' + x-preferred: true + x-providerName: twitter.com + x-serviceName: legacy + x-unofficialSpec: true +paths: + /account/settings.json: + get: + description: |- + Returns settings (including + current trend, geo and sleep time information) for the authenticating user. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/account/settings' + operationId: account.settings.get + responses: + '200': + description: Successful Response + parameters: + - description: |- + The Yahoo! Where On Earth ID to use as the user's default trend location. Global information is + available by using 1 as the WOEID. The woeid must be one of the locations returned by GET + trends/available. + + Example Values: 1 + in: query + name: trend_location_woeid + required: false + type: string + - description: |- + When set to true, t or 1, will enable sleep time for the user. Sleep time is the time when push or + SMS notifications should not be sent to the user. + + Example Values: true + in: query + name: sleep_time_enabled + required: false + type: string + - description: |- + The hour that sleep time should begin if it is enabled. The value for this parameter should be + provided in ISO8601 format (i.e. 00-23). The time is considered to be in the same timezone as the + user's time_zone setting. + + Example Values: 13 + in: query + name: start_sleep_time + required: false + type: string + - description: |- + The hour that sleep time should end if it is enabled. The value for this parameter should be + provided in ISO8601 format (i.e. 00-23). The time is considered to be in the same timezone as the + user's time_zone setting. + + Example Values: 13 + in: query + name: end_sleep_time + required: false + type: string + - description: |- + The timezone dates and times should be displayed in for the user. The timezone must be one of the + Rails TimeZone names. + + Example Values: Europe/Copenhagen, Pacific/Tongatapu + in: query + name: time_zone + required: false + type: string + - description: |- + The language which Twitter should render in for this user. The language must be specified by the + appropriate two letter ISO 639-1 representation. Currently supported languages are provided by GET + help/languages. + + Example Values: it, en, es + in: query + name: lang + required: false + type: string + post: + description: |- + Updates the + authenticating user's settings. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/account/settings' + operationId: account.settings.post + responses: + '200': + description: Successful Response + /account/update_delivery_device.json: + parameters: + - description: |- + Must be one of: sms, none. + + Example Values: sms + in: query + name: device + required: true + type: string + - description: |- + When set to either true, t or 1, each tweet will include a node called "entities,". This node + offers a variety of metadata about the tweet in a discreet structure, including: user_mentions, + urls, and hashtags. While entities are opt-in on timelines at present, they will be made a default + component of output in the future. See Tweet Entities for more detail on entities. + + Example Values: true + in: query + name: include_entities + required: false + type: string + post: + description: |- + Sets which + device Twitter delivers updates to for the authenticating user. Sending none as the device parameter + will disable SMS updates. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/account/update_delivery_device' + operationId: account.update_delivery_device + responses: + '200': + description: Successful Response + /account/update_profile.json: + parameters: + - description: |- + Full name associated with the profile. Maximum of 20 characters. + + Example Values: Marcel Molina + in: query + name: name + required: false + type: string + - description: |- + URL associated with the profile. Will be prepended with "http://" if not present. Maximum of 100 + characters. + + Example Values: http://project.ioni.st + in: query + name: url + required: false + type: string + - description: |- + The city or country describing where the user of the account is located. The contents are not + normalized or geocoded in any way. Maximum of 30 characters. + + Example Values: San Francisco, CA + in: query + name: location + required: false + type: string + - description: |- + A description of the user owning the account. Maximum of 160 characters. + + Example Values: Flipped my wig at age 22 and it never grew back. Also: I work at Twitter. + in: query + name: description + required: false + type: string + - description: |- + The entities node will not be included when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + post: + description: |- + Sets values that + users are able to set under the Account tab of their settings page. Only the parameters specified + will be updated. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/account/update_profile' + operationId: account.update_profile + responses: + '200': + description: Successful Response + /account/update_profile_background_image.json: + parameters: + - default: multipart/form-data + description: Content type header + in: header + name: Content-Type + required: true + type: string + - description: |- + Whether or not to tile the background image. If set to true, t or 1 the background image will + be displayed tiled. The image will not be tiled otherwise. + in: query + name: tile + required: false + type: string + - description: |- + Determines whether to display the profile background image or not. When set to true, t or 1 the + background image will be displayed if an image is being uploaded with the request, or has been + uploaded previously. An error will be returned if you try to use a background image when one is + not being uploaded or does not exist. If this parameter is defined but set to anything other + than true, t or 1, the background image will stop being used. + in: query + name: use + required: false + type: string + - description: |- + The entities node will not be included when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + post: + description: |- + Updates the authenticating user's profile background image. This method can also be used to enable + or disable the profile background image. Although each parameter is marked as optional, at least one + of image, tile or use must be provided when making this request. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/account/update_profile_background_image' + operationId: accounts.update_profile_background_image + parameters: [] + responses: + '200': + description: Successful Response + /account/update_profile_colors.json: + parameters: + - description: 'Profile background color. Example Values: 3D3D3D' + in: query + name: profile_background_color + required: false + type: string + - description: 'Profile link color.Example Values: 0000FF' + in: query + name: profile_link_color + required: false + type: string + - description: 'Profile sidebar''s border color. Example Values: 0F0F0F' + in: query + name: profile_sidebar_border_color + required: false + type: string + - description: 'Profile sidebar''s background color. Example Values: 00FF00' + in: query + name: profile_sidebar_fill_color + required: false + type: string + - description: 'Profile text color. Example Values: 000000' + in: query + name: profile_text_color + required: false + type: string + - description: 'The entities node will not be included when set to false. Example Values: false' + in: query + name: include_entities + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + post: + description: |- + Sets one or + more hex values that control the color scheme of the authenticating user's profile page on + twitter.com. + Each parameter's value must be a valid hexidecimal value, and may be either three or six characters + (ex: #fff or #ffffff). + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/account/update_profile_colors' + operationId: accounts.update_profile_colors + responses: + '200': + description: Successful Response + /account/update_profile_image.json: + parameters: + - default: multipart/form-data + description: Content type header + in: header + name: Content-Type + required: true + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + post: + description: |- + Updates the + authenticating user's profile image. Note that this method expects raw multipart data, not a URL to + an image. This method asynchronously processes the uploaded file before updating the user's profile + image URL. You can either update your local cache the next time you request the user's information, + or, at least 5 seconds after uploading the image, ask for the updated URL using GET + users/profile_image/:screen_name + (https://dev.twitter.com/docs/api/1/get/users/profile_image/:screen_name). + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/account/update_profile_image' + operationId: accounts.update_profile_image + parameters: [] + responses: + '200': + description: Successful Response + /application/rate_limit_status.json: + get: + description: |- + Returns the + current rate limits for + methods belonging to the specified resource families. + + Each 1.1 API resource belongs to a "resource family" which is indicated in its method documentation. + You can typically determine a method's resource family from the first component of the path after + the resource version. + + This method responds with a map of methods belonging to the families specified by the resources + parameter, the current remaining uses for each of those resources within the current rate limiting + window, and its expiration time in epoch time. It also includes a rate_limit_context field that + indicates the current access token context. + + You may also issue requests to this method without any parameters to receive a map of all rate + limited GET methods. If your application only uses a few of methods, please explicitly provide a + resources parameter with the specified resource families you work with. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/application/rate_limit_status' + operationId: application.rate_limit_status + responses: + '200': + description: Successful Response + parameters: + - description: |- + A comma-separated list of resource families you want to know the current rate limit disposition + for. For best performance, only specify the resource families pertinent to your application.Example + Values: statuses,friends,trends,help + in: query + name: resources + required: false + type: string + /blocks/create.json: + parameters: + - description: |- + The entities node will not be included when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + post: + description: |- + Blocks the specified user from + following the authenticating user. In addition the blocked user will not show in the authenticating + users mentions or timeline (unless retweeted by another user). If a follow or friend relationship + exists it is destroyed. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/blocks/create' + operationId: blocks.create + responses: + '200': + description: Successful Response + /blocks/destroy.json: + parameters: + - description: |- + The entities node will not be included when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + post: + description: |- + Un-blocks the user specified + in the ID parameter for the authenticating user. Returns the un-blocked user in the requested format + when successful. If relationships existed before the block was instated, they will not be restored. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/blocks/destroy' + operationId: blocks.destroy + responses: + '200': + description: Successful Response + /blocks/ids.json: + get: + description: |- + Returns an array of numeric user + ids the authenticating user is blocking. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/blocks/ids' + operationId: blocks.ids + responses: + '200': + description: Successful Response + parameters: + - description: |- + Many programming environments will not consume our ids due to their size. Provide this option to + have ids returned as strings instead. Read more about Twitter IDs, JSON and Snowflake. + + Example Values: true + in: query + name: stringify_ids + required: false + type: string + - description: |- + Causes the list of blocked users to be broken into pages of no more than 5000 IDs at a time. The + number of IDs returned is not guaranteed to be 5000 as suspended users are filtered out after + connections are queried. If no cursor is provided, a value of -1 will be assumed, which is the first + "page." + + The response from the API will include a previous_cursor and next_cursor to allow paging back and + forth. See Using cursors to navigate collections for more information. + + Example Values: 12893764510938 + in: query + name: cursor + required: false + type: string + /blocks/list.json: + get: + description: |- + Allows one to enable or + disable retweets and device notifications from the specified user. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/blocks/list' + operationId: blocks.list + responses: + '200': + description: Successful Response + parameters: + - description: 'The entities node will not be included when set to false. Example Values: false' + in: query + name: include_entities + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + - description: |- + Causes the list of blocked users to be broken into pages of no more than 5000 IDs at a time. The + number of IDs returned is not guaranteed to be 5000 as suspended users are filtered out after + connections are queried. If no cursor is provided, a value of -1 will be assumed, which is the first + "page." + + The response from the API will include a previous_cursor and next_cursor to allow paging back and + forth. See Using cursors to navigate collections for more information. + + Example Values: 12893764510938 + in: query + name: cursor + required: false + type: string + /direct_messages.json: + get: + description: |- + Returns the 20 most recent + direct messages sent to the authenticating user. Includes detailed information about the sender and + recipient user. You can request up to 200 direct messages per call, up to a maximum of 800 incoming + DMs. + + Important: This method requires an access token with RWD (read, write and direct message) + permissions. + Consult The Application Permission Model for more information. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/direct_messages' + operationId: direct_messages + responses: + '200': + description: Successful Response + parameters: + - description: |- + Specifies the number of direct messages to try and retrieve, up to a maximum of 200. The value of + count is best thought of as a limit to the number of Tweets to return because suspended or deleted + content is removed after the count has been applied. + + Example Values: 5 + in: query + name: count + required: false + type: string + - description: |- + Returns results with an ID greater than (that is, more recent than) the specified ID. There are + limits to the number of Tweets which can be accessed through the API. If the limit of Tweets has + occured since the since_id, the since_id will be forced to the oldest ID available. + Example Values: 12345 + in: query + name: since_id + required: false + type: string + - description: |- + Returns results with an ID less than (that is, older than) or equal to the specified ID. + + Example Values: 54321 + in: query + name: max_id + required: false + type: string + - description: |- + The entities node will not be included when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + - description: |- + Specifies the page of results to retrieve. + + Example Values: 3 + in: query + name: page + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + /direct_messages/destroy.json: + parameters: + - description: |- + The ID of the direct message to delete. + + Example Values: 1270516771 + in: query + name: id + required: true + type: string + - description: |- + The entities node will not be included when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + post: + description: |- + Destroys the direct + message specified in the required ID parameter. The authenticating user must be the recipient of the + specified direct message. + + Important: This method requires an access token with RWD (read, write and direct message) + permissions. + Consult The Application Permission Model for more information. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/direct_messages/destroy' + operationId: direct_messages.destroy + responses: + '200': + description: Successful Response + /direct_messages/new.json: + parameters: + - description: |- + The text of your direct message. Be sure to URL encode as necessary, and keep the message under 140 + characters. + + Example Values: Meet me behind the cafeteria after school + in: query + name: text + required: true + type: string + post: + description: |- + Sends a new direct + message to the specified user from the authenticating user. Requires both the user and text + parameters and must be a POST. Returns the sent message in the requested format if successful. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/direct_messages/new' + operationId: direct_messages.new + responses: + '200': + description: Successful Response + /direct_messages/sent.json: + get: + description: |- + Returns the 20 most + recent direct messages sent by the authenticating user. Includes detailed information about the + sender and recipient user. You can request up to 200 direct messages per call, up to a maximum of + 800 outgoing DMs. + + Important: This method requires an access token with RWD (read, write and direct message) + permissions. Consult The Application Permission Model for more information. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/direct_messages/sent' + operationId: direct_messages.sent + responses: + '200': + description: Successful Response + parameters: + - description: |- + Specifies the number of direct messages to try and retrieve, up to a maximum of 200. The value of + count is best thought of as a limit to the number of Tweets to return because suspended or deleted + content is removed after the count has been applied. + + Example Values: 5 + in: query + name: count + required: false + type: string + - description: |- + Returns results with an ID greater than (that is, more recent than) the specified ID. There are + limits to the number of Tweets which can be accessed through the API. If the limit of Tweets has + occured since the since_id, the since_id will be forced to the oldest ID available. + + Example Values: 12345 + in: query + name: since_id + required: false + type: string + - description: |- + Returns results with an ID less than (that is, older than) or equal to the specified ID. + + Example Values: 54321 + in: query + name: max_id + required: false + type: string + - description: |- + The entities node will not be included when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + - description: |- + Specifies the page of results to retrieve. + + Example Values: 3 + in: query + name: page + required: false + type: string + /direct_messages/show.json: + get: + description: |- + Returns a single direct + message, specified by an id parameter. Like the /1.1/direct_messages.format request, this method + will include the user objects of the sender and recipient. + + Important: This method requires an access token with RWD (read, write and direct message) + permissions. + Consult The Application Permission Model for more information. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/direct_messages/show' + operationId: direct_messages.show + responses: + '200': + description: Successful Response + parameters: + - description: |- + The ID of the direct message. + + Example Values: 587424932 + in: query + name: id + required: true + type: string + /favorites/create.json: + parameters: + - description: |- + The numerical ID of the desired status. + + Example Values: 123 + in: query + name: id + required: true + type: string + - description: |- + The entities node will be omitted when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + post: + description: |- + Favorites the status + specified in the ID parameter as the authenticating user. Returns the favorite status when + successful. + + This process invoked by this method is asynchronous. The immediately returned status may not + indicate the resultant favorited status of the tweet. A 200 OK response from this method will + indicate whether the intended action was successful or not. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/favorites/create' + operationId: favorites.create + responses: + '200': + description: Successful Response + /favorites/destroy.json: + parameters: + - description: |- + The numerical ID of the desired status. + + Example Values: 123 + in: query + name: id + required: true + type: string + - description: |- + The entities node will be omitted when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + post: + description: |- + Un-favorites the status + specified in the ID parameter as the authenticating user. Returns the un-favorited status in the + requested format when successful. + + This process invoked by this method is asynchronous. The immediately returned status may not + indicate the resultant favorited status of the tweet. A 200 OK response from this method will + indicate whether the intended action was successful or not. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/favorites/destroy' + operationId: favorites.destroy + responses: + '200': + description: Successful Response + /favorites/list.json: + get: + description: |- + Returns the 20 most recent + Tweets favorited by the authenticating or specified user. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/favorites/list' + operationId: favorites.list + responses: + '200': + description: Successful Response + parameters: + - description: |- + Specifies the number of records to retrieve. Must be less than or equal to 200. Defaults to 20. + + Example Values: 5 + in: query + name: count + required: false + type: string + - description: |- + Returns results with an ID greater than (that is, more recent than) the specified ID. There are + limits to the number of Tweets which can be accessed through the API. If the limit of Tweets has + occured since the since_id, the since_id will be forced to the oldest ID available. + + Example Values: 12345 + in: query + name: since_id + required: false + type: string + - description: |- + Returns results with an ID less than (that is, older than) or equal to the specified ID. + + Example Values: 54321 + in: query + name: max_id + required: false + type: string + - description: |- + The entities node will be omitted when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + /followers/ids.json: + get: + description: |- + Returns a cursored collection + of user IDs for every user following the specified user. + + At this time, results are ordered with the most recent following first Ãĸâ‚Ŧ” however, this ordering is + subject to unannounced change and eventual consistency issues. Results are given in groups of 5,000 + user IDs and multiple "pages" of results can be navigated through using the next_cursor value in + subsequent requests. See Using cursors to navigate collections for more information. + + This method is especially powerful when used in conjunction with GET users/lookup, a method that + allows you to convert user IDs into full user objects in bulk. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/followers/ids' + operationId: followers.ids + responses: + '200': + description: Successful Response + parameters: + - description: |- + Many programming environments will not consume our Tweet ids due to their size. Provide this option + to have ids returned as strings instead. Example Values: true + in: query + name: stringify_ids + required: false + type: string + - description: |- + Causes the list of connections to be broken into pages of no more than 5000 IDs at a time. The + number of IDs returned is not guaranteed to be 5000 as suspended users are filtered out after + connections are queried. If no cursor is provided, a value of -1 will be assumed, which is the first + "page." + + The response from the API will include a previous_cursor and next_cursor to allow paging back and + forth.Example Values: 12893764510938 + in: query + name: cursor + required: false + type: string + /friends/ids.json: + get: + description: |- + Returns a cursored collection of + user IDs for every user the specified user is following (otherwise known as their "friends"). + + At this time, results are ordered with the most recent following first Ãĸâ‚Ŧ” however, this ordering is + subject to unannounced change and eventual consistency issues. Results are given in groups of 5,000 + user IDs and multiple "pages" of results can be navigated through using the next_cursor value in + subsequent requests. See Using cursors to navigate collections for more information. + + This method is especially powerful when used in conjunction with GET users/lookup, a method that + allows you to convert user IDs into full user objects in bulk. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/friends/ids' + operationId: friends.ids + responses: + '200': + description: Successful Response + parameters: + - description: |- + Many programming environments will not consume our Tweet ids due to their size. Provide this option + to have ids returned as strings instead. Example Values: true + in: query + name: stringify_ids + required: false + type: string + - description: |- + Causes the list of connections to be broken into pages of no more than 5000 IDs at a time. The + number of IDs returned is not guaranteed to be 5000 as suspended users are filtered out after + connections are queried. If no cursor is provided, a value of -1 will be assumed, which is the first + "page." + + The response from the API will include a previous_cursor and next_cursor to allow paging back and + forth.Example Values: 12893764510938 + in: query + name: cursor + required: false + type: string + /friendships/create.json: + parameters: + - description: 'Enable notifications for the target user. Example Values: true' + in: query + name: follow + required: false + type: string + post: + description: |- + Allows the authenticating + users to follow the user specified in the ID parameter. + + Returns the befriended user in the requested format when successful. Returns a string describing the + failure condition when unsuccessful. If you are already friends with the user a HTTP 403 may be + returned, though for performance reasons you may get a 200 OK message even if the friendship already + exists. + + Actions taken in this method are asynchronous and changes will be eventually consistent. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/friendships/create' + operationId: friendships.create + responses: + '200': + description: Successful Response + /friendships/destroy.json: + parameters: [] + post: + description: |- + Allows the + authenticating + user to unfollow the user specified in the ID parameter. + + Returns the unfollowed user in the requested format when successful. Returns a string describing the + failure condition when unsuccessful. + + Actions taken in this method are asynchronous and changes will be eventually consistent. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/friendships/destroy' + operationId: friendships.destroy + responses: + '200': + description: Successful Response + /friendships/incoming.json: + get: + description: |- + Returns the + relationships + of the authenticating user to the comma-separated list of up to 100 screen_names or user_ids + provided. Values for connections can be: following, following_requested, followed_by, none. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/friendships/incoming' + operationId: friendships.incoming + responses: + '200': + description: Successful Response + parameters: + - description: |- + Many programming environments will not consume our Tweet ids due to their size. Provide this option + to have ids returned as strings instead. Example Values: true + in: query + name: stringify_ids + required: false + type: string + - description: |- + Causes the list of connections to be broken into pages of no more than 5000 IDs at a time. The + number of IDs returned is not guaranteed to be 5000 as suspended users are filtered out after + connections are queried. If no cursor is provided, a value of -1 will be assumed, which is the first + "page." + + The response from the API will include a previous_cursor and next_cursor to allow paging back and + forth.Example Values: 12893764510938 + in: query + name: cursor + required: false + type: string + /friendships/lookup.json: + get: + description: |- + Returns the relationships + of the authenticating user to the comma-separated list of up to 100 screen_names or user_ids + provided. Values for connections can be: following, following_requested, followed_by, none. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/friendships/lookup' + operationId: friendships.lookup + responses: + '200': + description: Successful Response + parameters: [] + /friendships/outgoing.json: + get: + description: |- + Returns a collection of + numeric IDs for every protected user for whom the authenticating user has a pending follow request. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/friendships/outgoing' + operationId: friendships.outgoing + responses: + '200': + description: Successful Response + parameters: + - description: |- + Many programming environments will not consume our Tweet ids due to their size. Provide this option + to have ids returned as strings instead. Example Values: true + in: query + name: stringify_ids + required: false + type: string + - description: |- + Causes the list of connections to be broken into pages of no more than 5000 IDs at a time. The + number of IDs returned is not guaranteed to be 5000 as suspended users are filtered out after + connections are queried. If no cursor is provided, a value of -1 will be assumed, which is the first + "page." + + The response from the API will include a previous_cursor and next_cursor to allow paging back and + forth.Example Values: 12893764510938 + in: query + name: cursor + required: false + type: string + /friendships/show.json: + get: + description: |- + Returns detailed information + about the relationship between two arbitrary users. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/friendships/show' + operationId: friendships.show + responses: + '200': + description: Successful Response + parameters: + - description: |- + The user_id of the subject user. + + Example Values: 3191321 + in: query + name: source_id + required: false + type: string + - description: |- + The screen_name of the subject user. + + Example Values: raffi + in: query + name: source_screen_name + required: false + type: string + - description: |- + The user_id of the target user. + + Example Values: 20 + in: query + name: target_id + required: true + type: string + - description: |- + The screen_name of the target user. + + Example Values: noradio + in: query + name: target_screen_name + required: true + type: string + /friendships/update.json: + parameters: + - description: 'Enable/disable device notifications from the target user. Example Values: true, false' + in: query + name: device + required: true + type: string + - description: 'Enable/disable retweets from the target user. Example Values: true, false' + in: query + name: retweets + required: true + type: string + post: + description: |- + Allows one to enable or + disable retweets and device notifications from the specified user. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/friendships/update' + operationId: friendships.update + responses: + '200': + description: Successful Response + '/geo/id/{place_id}.json': + get: + description: |- + Returns all the + information about a known place.Example Values: df51dec6f4ee2b2c + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/geo/id/%3Aplace_id' + operationId: geo.place_id + responses: + '200': + description: Successful Response + parameters: + - description: |- + A place in the world. These IDs can be retrieved from geo/reverse_geocode. + + Example Values: df51dec6f4ee2b2c + in: path + name: place_id + required: true + type: string + /geo/places.json: + parameters: + - description: |- + This parameter searches for places which have this given street address. There are other + well-known, and application specific attributes available. Custom attributes are also permitted. + Learn more about Place Attributes. + + Example Values: 795%20Folsom%20St + in: query + name: 'attribute:street_address' + required: false + type: string + - description: 'If supplied, the response will use the JSONP format with a callback of the given name.' + in: query + name: callback + required: false + type: string + post: + description: |- + Creates a new place object at the + given latitude and longitude. + + Before creating a place you need to query GET geo/similar_places with the latitude, longitude and + name of the place you wish to create. The query will return an array of places which are similar to + the one you wish to create, and a token. If the place you wish to create isn't in the returned array + you can use the token with this method to create a new one. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/geo/place' + operationId: geo.places + responses: + '200': + description: Successful Response + /geo/reverse_geocode.json: + get: + description: |- + Given a latitude and a + longitude, searches for up to 20 places that can be used as a place_id when updating a status. + + This request is an informative call and will deliver generalized results about geography + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/geo/reverse_geocode' + operationId: geo.reverse_geocode + responses: + '200': + description: Successful Response + parameters: + - description: |- + The latitude to search around. This parameter will be ignored unless it is inside the range -90.0 + to +90.0 (North is positive) inclusive. It will also be ignored if there isn't a corresponding long + parameter. + + Example Values: 37.7821120598956 + in: query + name: lat + required: true + type: string + - description: |- + The longitude to search around. The valid ranges for longitude is -180.0 to +180.0 (East is + positive) inclusive. This parameter will be ignored if outside that range, if it is not a number, if + geo_enabled is disabled, or if there not a corresponding lat parameter. + + Example Values: -122.400612831116 + in: query + name: long + required: true + type: string + - description: |- + A hint on the "region" in which to search. If a number, then this is a radius in meters, but it can + also take a string that is suffixed with ft to specify feet. If this is not passed in, then it is + assumed to be 0m. If coming from a device, in practice, this value is whatever accuracy the device + has measuring its location (whether it be coming from a GPS, WiFi triangulation, etc.). + + Example Values: 5ft + in: query + name: accuracy + required: false + type: string + - description: |- + This is the minimal granularity of place types to return and must be one of: poi, neighborhood, + city, admin or country. If no granularity is provided for the request neighborhood is assumed. + Setting this to city, for example, will find places which have a type of city, admin or country. + + Example Values: city + in: query + name: granularity + required: false + type: string + - description: |- + A hint as to the number of results to return. This does not guarantee that the number of results + returned will equal max_results, but instead informs how many "nearby" results to return. Ideally, + only pass in the number of places you intend to display to the user here. + + Example Values: 3 + in: query + name: max_results + required: false + type: string + - description: 'If supplied, the response will use the JSONP format with a callback of the given name.' + in: query + name: callback + required: false + type: string + /geo/search.json: + get: + description: |- + Search for places that can be + attached to a statuses/update. Given a latitude and a longitude pair, an IP address, or a name, this + request will return a list of all the valid places that can be used as the place_id when updating a + status. + + Conceptually, a query can be made from the user's location, retrieve a list of places, have the user + validate the location he or she is at, and then send the ID of this location with a call to POST + statuses/update. + + This is the recommended method to use find places that can be attached to statuses/update. Unlike + GET geo/reverse_geocode which provides raw data access, this endpoint can potentially re-order + places with regards to the user who is authenticated. This approach is also preferred for + interactive place matching with the user. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/geo/search' + operationId: geo.search + responses: + '200': + description: Successful Response + parameters: + - description: |- + A hint on the "region" in which to search. If a number, then this is a radius in meters, but it can + also take a string that is suffixed with ft to specify feet. If this is not passed in, then it is + assumed to be 0m. If coming from a device, in practice, this value is whatever accuracy the device + has measuring its location (whether it be coming from a GPS, WiFi triangulation, etc.). + + Example Values: 5ft + in: query + name: accuracy + required: false + type: string + - description: |- + This is the minimal granularity of place types to return and must be one of: poi, neighborhood, + city, admin or country. If no granularity is provided for the request neighborhood is assumed. + Setting this to city, for example, will find places which have a type of city, admin or country. + + Example Values: city + in: query + name: granularity + required: false + type: string + - description: |- + This is the place_id which you would like to restrict the search results to. Setting this value + means only places within the given place_id will be found. + + Specify a place_id. For example, to scope all results to places within "San Francisco, CA USA", you + would specify a place_id of "5a110d312052166f" + + Example Values: 247f43d441defc03 + in: query + name: contained_within + required: false + type: string + - description: |- + This parameter searches for places which have this given street address. There are other + well-known, and application specific attributes available. Custom attributes are also permitted. + Learn more about Place Attributes. + + Example Values: 795%20Folsom%20St + in: query + name: 'attribute:street_address' + required: false + type: string + - description: 'If supplied, the response will use the JSONP format with a callback of the given name.' + in: query + name: callback + required: false + type: string + /geo/similar_places.json: + get: + description: |- + Locates places near the + given coordinates which are similar in name. + + Conceptually you would use this method to get a list of known places to choose from first. Then, if + the desired place doesn't exist, make a request to POST geo/place to create a new one. + + The token contained in the response is the token needed to be able to create a new place. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/geo/similar_places' + operationId: geo.similar_places + responses: + '200': + description: Successful Response + parameters: + - description: |- + This is the place_id which you would like to restrict the search results to. Setting this value + means only places within the given place_id will be found. + + Specify a place_id. For example, to scope all results to places within "San Francisco, CA USA", you + would specify a place_id of "5a110d312052166f" + + Example Values: 247f43d441defc03 + in: query + name: contained_within + required: false + type: string + - description: |- + This parameter searches for places which have this given street address. There are other + well-known, and application specific attributes available. Custom attributes are also permitted. + Learn more about Place Attributes. + + Example Values: 795%20Folsom%20St + in: query + name: 'attribute:street_address' + required: false + type: string + - description: 'If supplied, the response will use the JSONP format with a callback of the given name.' + in: query + name: callback + required: false + type: string + /help/configuration.json: + get: + description: |- + Returns the current + configuration used by Twitter including twitter.com slugs which are not usernames, maximum photo + resolutions, and t.co URL lengths. + + It is recommended applications request this endpoint when they are loaded, but no more than once a + day. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/help/configuration' + operationId: help.configurations + responses: + '200': + description: Successful Response + parameters: [] + /help/languages.json: + get: + description: |- + Returns the list of languages + supported by Twitter along with their ISO 639-1 code. The ISO 639-1 code is the two letter value to + use if you include lang with any of your requests. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/help/languages' + operationId: help.languages + responses: + '200': + description: Successful Response + parameters: [] + /help/privacy.json: + get: + description: Returns Twitter's Privacy Policy + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/help/privacy' + operationId: help.privacy + responses: + '200': + description: Successful Response + parameters: [] + /help/tos.json: + get: + description: |- + Returns the Twitter Terms of Service + in the requested format. These are not the same as the Developer Rules of the Road. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/help/tos' + operationId: help.tos + responses: + '200': + description: Successful Response + parameters: [] + /lists/create.json: + parameters: + - description: |- + The name for the list.A list's name must start with a letter and can consist only of 25 or fewer + letters, numbers, "-", or "_" characters. + in: query + name: name + required: true + type: string + - description: |- + Whether your list is public or private. Values can be public or private. If no mode is specified + the list will be public. + in: query + name: mode + required: false + type: string + - description: The description to give the list. + in: query + name: description + required: false + type: string + post: + description: |- + Creates a new list for the + authenticated user. Note that you can't create more than 20 lists per account. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/lists/create' + operationId: lists.create + responses: + '200': + description: Successful Response + /lists/destroy.json: + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + post: + description: |- + Deletes the specified list. + The authenticated user must own the list to be able to destroy it. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/lists/destroy' + operationId: lists.destroy + responses: + '200': + description: Successful Response + /lists/list.json: + get: + description: |- + Returns all lists the + authenticating or specified user subscribes to, including their own. The user is specified using the + user_id or screen_name parameters. If no user is given, the authenticating user is used. + + This method used to be GET lists in version 1.0 of the API and has been renamed for consistency with + other call. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/lists/list' + operationId: lists.list + responses: + '200': + description: Successful Response + parameters: + - description: |- + The screen name of the user for whom to return results for. Helpful for disambiguating when a valid + screen name is also a user ID. + + Example Values: noradio + in: query + name: screen_name + required: true + type: string + - description: |- + The ID of the user for whom to return results for. Helpful for disambiguating when a valid user ID + is also a valid screen name. + + Example Values: 12345 + + Note:: Specifies the ID of the user to get lists from. Helpful for disambiguating when a valid user + ID is also a valid screen name. + in: query + name: user_id + required: true + type: string + /lists/members.json: + get: + description: |- + Returns the members of the + specified list. Private list members will only be shown if the authenticated user owns the specified + list. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/lists/members' + operationId: lists.members + responses: + '200': + description: Successful Response + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + - description: |- + The entities node will be disincluded when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + - description: |- + Causes the collection of list members to be broken into "pages" of somewhat consistent size. If no + cursor is provided, a value of -1 will be assumed, which is the first "page." + + The response from the API will include a previous_cursor and next_cursor to allow paging back and + forth. See Using cursors to navigate collections for more information. + + Example Values: 12893764510938 + in: query + name: cursor + required: false + type: string + /lists/members/create.json: + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + post: + description: |- + Add a member to a list. + The authenticated user must own the list to be able to add members to it. Note that lists can't have + more than 500 members. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/lists/members/create' + operationId: lists.members.create + responses: + '200': + description: Successful Response + /lists/members/create_all.json: + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + - description: 'A comma separated list of user IDs, up to 100 are allowed in a single request.' + in: query + name: user_id + required: false + type: string + - description: 'A comma separated list of screen names, up to 100 are allowed in a single request.' + in: query + name: screen_name + required: false + type: string + post: + description: |- + Adds multiple + members to a list, by specifying a comma-separated list of member ids or screen names. The + authenticated user must own the list to be able to add members to it. Note that lists can't have + more than 500 members, and you are limited to adding up to 100 members to a list at a time with this + method. + + Please note that there can be issues with lists that rapidly remove and add memberships. Take care + when using these methods such that you are not too rapidly switching between removals and adds on + the same list. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/lists/members/create_all' + operationId: lists.members.create_all + responses: + '200': + description: Successful Response + /lists/members/destroy.json: + parameters: + - description: The numerical id of the list. + in: query + name: list_id + required: true + type: string + - description: |- + You can identify a list by its slug instead of its numerical id. If you decide to do so, note + that you'll also have to specify the list owner using the owner_id or owner_screen_name + parameters. + in: query + name: slug + required: true + type: string + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + - description: |- + The ID of the user to remove from the list. Helpful for disambiguating when a valid user ID is also + a valid screen name. + in: query + name: user_id + required: false + type: string + - description: |- + The screen name of the user for whom to remove from the list. Helpful for disambiguating when a + valid screen name is also a user ID. + in: query + name: screen_name + required: false + type: string + post: + description: |- + Removes the specified + member from the list. The authenticated user must be the list's owner to remove members from the + list. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy' + operationId: lists.members.destroy + responses: + '200': + description: Successful Response + /lists/members/destroy_all.json: + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + - description: 'A comma separated list of screen names, up to 100 are allowed in a single request.' + in: query + name: screen_name + required: false + type: string + - description: 'A comma separated list of user IDs, up to 100 are allowed in a single request.' + in: query + name: user_id + required: false + type: string + post: + description: |- + Removes multiple + members from a list, by specifying a comma-separated list of member ids or screen names. The + authenticated user must own the list to be able to remove members from it. Note that lists can't + have more than 500 members, and you are limited to removing up to 100 members to a list at a time + with this method. + + Please note that there can be issues with lists that rapidly remove and add memberships. Take care + when using these methods such that you are not too rapidly switching between removals and adds on + the same list. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy_all' + operationId: lists.members.destroy_all + responses: + '200': + description: Successful Response + /lists/members/show.json: + get: + description: |- + Check if the specified + user is a member of the specified list. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/lists/members/show' + operationId: lists.members.show + responses: + '200': + description: Successful Response + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + - description: |- + When set to either true, t or 1, each tweet will include a node called "entities". This node offers + a variety of metadata about the tweet in a discreet structure, including: user_mentions, urls, and + hashtags. While entities are opt-in on timelines at present, they will be made a default component + of output in the future. + in: query + name: include_entities + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + /lists/memberships.json: + get: + description: |- + Returns the lists the + specified user has been added to. If user_id or screen_name are not provided the memberships for the + authenticating user are returned. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/lists/memberships' + operationId: lists.memberships + responses: + '200': + description: Successful Response + parameters: + - description: |- + The ID of the user for whom to return results for. Helpful for disambiguating when a valid user ID + is also a valid screen name. + in: query + name: user_id + required: false + type: string + - description: |- + The screen name of the user for whom to return results for. Helpful for disambiguating when a valid + screen name is also a user ID. + in: query + name: screen_name + required: false + type: string + - description: |- + Breaks the results into pages. A single page contains 20 lists. Provide a value of -1 to begin + paging. Provide values as returned in the response body's next_cursor and previous_cursor attributes + to page back and forth in the list. + in: query + name: cursor + required: false + type: string + - description: |- + When set to true, t or 1, will return just lists the authenticating user owns, and the user + represented by user_id or screen_name is a member of. + in: query + name: filter_to_owned_lists + required: false + type: string + /lists/show.json: + get: + description: |- + Returns the specified list. + Private lists will only be shown if the authenticated user owns the specified list. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/lists/show' + operationId: lists.show + responses: + '200': + description: Successful Response + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + /lists/statuses.json: + get: + description: |- + Returns tweet timeline for + members of the specified list. Retweets are included by default. You can use the include_rts=false + parameter to omit retweet objects. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/lists/statuses' + operationId: lists.statuses + responses: + '200': + description: Successful Response + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + - description: |- + Returns results with an ID greater than (that is, more recent than) the specified ID. There are + limits to the number of Tweets which can be accessed through the API. If the limit of Tweets has + occured since the since_id, the since_id will be forced to the oldest ID available. + in: query + name: since_id + required: false + type: string + - description: 'Returns results with an ID less than (that is, older than) or equal to the specified ID.' + in: query + name: max_id + required: false + type: string + - description: Specifies the number of results to retrieve per "page. + in: query + name: count + required: false + type: string + - description: |- + Entities are ON by default in API 1.1, each tweet includes a node called "entities". This node + offers a variety of metadata about the tweet in a discreet structure, including: user_mentions, + urls, and hashtags. You can omit entities from the result by using include_entities=false + in: query + name: include_entities + required: false + type: string + - description: |- + When set to either true, t or 1, the list timeline will contain native retweets (if they exist) in + addition to the standard stream of tweets. The output format of retweeted tweets is identical to the + representation you see in home_timeline. + in: query + name: include_rts + required: true + type: string + /lists/subscribers.json: + get: + description: |- + Returns the subscribers of + the specified list. Private list subscribers will only be shown if the authenticated user owns the + specified list. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/lists/subscribers' + operationId: lists.subscribers + responses: + '200': + description: Successful Response + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + - description: |- + Breaks the results into pages. A single page contains 20 lists. Provide a value of -1 to begin + paging. Provide values as returned in the response body's next_cursor and previous_cursor attributes + to page back and forth in the list. + in: query + name: cursor + required: false + type: string + - description: |- + When set to either true, t or 1, each tweet will include a node called "entities". This node offers + a variety of metadata about the tweet in a discreet structure, including: user_mentions, urls, and + hashtags. While entities are opt-in on timelines at present, they will be made a default component + of output in the future. See Tweet Entities for more details. + in: query + name: include_entities + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + /lists/subscribers/create.json: + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + post: + description: |- + Subscribes the + authenticated user to the specified list. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/create' + operationId: lists.subscribers.create + responses: + '200': + description: Successful Response + /lists/subscribers/destroy.json: + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + post: + description: |- + Unsubscribes the + authenticated user from the specified list. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/destroy' + operationId: lists.subscribers.destroy + responses: + '200': + description: Successful Response + /lists/subscribers/show.json: + get: + description: |- + Check if the specified + user is a subscriber of the specified list. Returns the user if they are subscriber. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/lists/subscribers/show' + operationId: lists.subscribers.show + responses: + '200': + description: Successful Response + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + - description: |- + When set to either true, t or 1, each tweet will include a node called "entities". This node offers + a variety of metadata about the tweet in a discreet structure, including: user_mentions, urls, and + hashtags. While entities are opt-in on timelines at present, they will be made a default component + of output in the future. See Tweet Entities for more details. + in: query + name: include_entities + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + /lists/subscriptions.json: + get: + description: |- + Obtain a collection of + the lists the specified user is subscribed to, 20 lists per page by default. Does not include the + user's own lists. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/lists/subscriptions' + operationId: lists.subscriptions + responses: + '200': + description: Successful Response + parameters: + - description: 'The amount of results to return per page. Defaults to 20. Maximum of 1,000 when using cursors.' + in: query + name: count + required: false + type: string + - description: |- + Breaks the results into pages. A single page contains 20 lists. Provide a value of -1 to begin + paging. Provide values as returned in the response body's next_cursor and previous_cursor attributes + to page back and forth in the list. It is recommended to always use cursors when the method supports + them. + in: query + name: cursor + required: false + type: string + /lists/update.json: + parameters: + - description: The screen name of the user who owns the list being requested by a slug. + in: query + name: owner_screen_name + required: false + type: string + - description: The user ID of the user who owns the list being requested by a slug. + in: query + name: owner_id + required: false + type: string + - description: The name for the list. + in: query + name: name + required: false + type: string + - description: |- + Whether your list is public or private. Values can be public or private. If no mode is specified + the list will be public. + in: query + name: mode + required: false + type: string + - description: The description to give the list. + in: query + name: description + required: false + type: string + post: + description: |- + Updates the specified list. The + authenticated user must own the list to be able to update it. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/lists/update' + operationId: lists.update + responses: + '200': + description: Successful Response + /saved_searches/create.json: + parameters: + - description: The query of the search the user would like to save. + in: query + name: query + required: true + type: string + post: + description: |- + Create a new saved + search for the authenticated user. A user may only have 25 saved searches. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/saved_searches/create' + operationId: saved_searches.create + responses: + '200': + description: Successful Response + '/saved_searches/destroy/{id}.json': + parameters: + - description: |- + The ID of the saved search. + + Example Values: 313006 + in: path + name: id + required: true + type: string + post: + description: |- + Destroys a + saved + search for the authenticating user. The authenticating user must be the owner of saved search id + being destroyed. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/saved_searches/destroy/%3Aid' + operationId: saved_searches.destroy + responses: + '200': + description: Successful Response + /saved_searches/list.json: + get: + description: |- + Returns the authenticated + user's saved search queries. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/saved_searches/list' + operationId: saved_searches.list + responses: + '200': + description: Successful Response + parameters: [] + '/saved_searches/show/{id}.json': + get: + description: |- + Returns the + authenticated user's saved search queries. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/saved_searches/show/%3Aid' + operationId: savedsearchesid + responses: + '200': + description: Successful Response + parameters: + - description: |- + The ID of the saved search. + + Example Values: 313006 + in: path + name: id + required: true + type: string + /search/tweets.json: + get: + description: |- + Returns a collection of + relevant Tweets matching a specified query. + + Please note that Twitter's search service and, by extension, the Search API is not meant to be an + exhaustive source of Tweets. Not all Tweets will be indexed or made available via the search + interface. + + In API v1.1, the response format of the Search API has been improved to return Tweet objects more + similar to the objects you'll find across the REST API and platform. You may need to tolerate some + inconsistencies and variance in perspectival values (fields that pertain to the perspective of the + authenticating user) and embedded user objects. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/search/tweets' + operationId: search.tweets + responses: + '200': + description: Successful Response + parameters: + - description: |- + A UTF-8, URL-encoded search query of 1,000 characters maximum, including operators. Queries may + additionally be limited by complexity.Example: @noradio. + in: query + name: q + required: true + type: string + - description: |- + Returns tweets by users located within a given radius of the given latitude/longitude. The location + is preferentially taking from the Geotagging API, but will fall back to their Twitter profile. The + parameter value is specified by "latitude,longitude,radius", where radius units must be specified as + either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to + geocode arbitrary locations; however you can use this geocode parameter to search near geocodes + directly. A maximum of 1,000 distinct "sub-regions" will be considered when using the radius + modifier. + in: query + name: geocode + required: false + type: string + - description: |- + Restricts tweets to the given language, given by an ISO 639-1 code. Language detection is + best-effort.Example Values: eu + in: query + name: lang + required: false + type: string + - description: |- + Specify the language of the query you are sending (only ja is currently effective). This is + intended for language-specific consumers and the default should work in the majority of + cases.Example Values: ja + in: query + name: locale + required: false + type: string + - description: |- + Optional. Specifies what type of search results you would prefer to receive. The current default is + "mixed." Valid values include: + * mixed: Include both popular and real time results in the response. + * recent: return only the most recent results in the response + * popular: return only the most popular results in the response. Example Values: mixed, recent, + popular + in: query + name: result_type + required: false + type: string + - description: |- + The number of tweets to return per page, up to a maximum of 100. Defaults to 15. This was formerly + the "rpp" parameter in the old Search API. Example Values: 100 + in: query + name: count + required: false + type: string + - description: |- + Returns tweets generated before the given date. Date should be formatted as YYYY-MM-DD. Keep in + mind that the search index may not go back as far as the date you specify here. Example Values: + 2012-09-01 + in: query + name: until + required: false + type: string + - description: |- + Returns results with an ID greater than (that is, more recent than) the specified ID. There are + limits to the number of Tweets which can be accessed through the API. If the limit of Tweets has + occured since the since_id, the since_id will be forced to the oldest ID available. Example Values: + 12345 + in: query + name: since_id + required: false + type: string + - description: |- + Returns results with an ID less than (that is, older than) or equal to the specified ID. Example + Values: 12345 + in: query + name: max_id + required: false + type: string + - description: 'The entities node will be disincluded when set to false. Example Values: false' + in: query + name: include_entities + required: false + type: string + - description: |- + If supplied, the response will use the JSONP format with a callback of the given name. The + usefulness of this parameter is somewhat diminished by the requirement of authentication for + requests to this endpoint. Example Values: processTweets + in: query + name: callback + required: false + type: string + '/statuses/destroy/{id}.json': + parameters: + - description: The numerical ID of the desired status. + in: path + name: id + required: true + type: string + - description: |- + When set to either true, t or 1, each tweet returned in a timeline will include a user object + including only the status authors numerical ID. Omit this parameter to receive the complete user + object. + in: query + name: trim_user + required: false + type: string + post: + description: |- + Destroys the status + specified by the required ID parameter. The authenticating user must be the author of the specified + status. Returns the destroyed status if successful. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/statuses/destroy/:id' + operationId: statuses.destroy + responses: + '200': + description: Successful Response + /statuses/home_timeline.json: + get: + description: |- + Returns a collection + of the most recent Tweets and retweets posted by the authenticating user and the users they follow. + The home timeline is central to how most users interact with the Twitter service. + + Up to 800 Tweets are obtainable on the home timeline. It is more volatile for users that follow many + users or follow users who tweet frequently. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/statuses/home_timeline' + operationId: statuses.home_timeline + responses: + '200': + description: Successful Response + parameters: + - description: Specifies the number of records to retrieve. Must be less than or equal to 200. + in: query + name: count + required: false + type: integer + - description: 'Returns results with an ID less than (that is, older than) or equal to the specified ID.' + format: int64 + in: query + name: max_id + required: false + type: integer + - description: |- + Returns results with an ID greater than (that is, more recent than) the specified ID. There are + limits to the number of Tweets which can be accessed through the API. If the limit of Tweets has + occured since the since_id, the since_id will be forced to the oldest ID available. + format: int64 + in: query + name: since_id + required: false + type: integer + - description: |- + When set to either true, t or 1, each tweet returned in a timeline will include a user object + including only the status authors numerical ID. Omit this parameter to receive the complete user + object. + in: query + name: trim_user + required: false + type: string + - description: |- + This parameter will prevent replies from appearing in the returned timeline. Using exclude_replies + with the count parameter will mean you will receive up-to count tweets Ãĸâ‚Ŧ” this is because the count + parameter retrieves that many tweets before filtering out retweets and replies. + in: query + name: exclude_replies + required: false + type: string + - description: |- + This parameter enhances the contributors element of the status response to include the screen_name + of the contributor. By default only the user_id of the contributor is included. + in: query + name: contributor_details + required: false + type: string + /statuses/mentions_timeline.json: + get: + description: |- + Returns the 20 + most recent mentions (tweets containing a users's @screen_name) for the authenticating user.The + timeline returned is the equivalent of the one seen when you view your mentions on twitter.com.This + method can only return up to 800 statuses.This method will include retweets in the JSON response + regardless of whether the include_rts parameter is set. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/statuses/mentions_timeline' + operationId: statuses.mentions.timeline + responses: + '200': + description: Successful Response + parameters: + - description: |- + Specifies the number of tweets to try and retrieve, up to a maximum of 200. The value of count is + best thought of as a limit to the number of tweets to return because suspended or deleted content is + removed after the count has been applied. We include retweets in the count, even if include_rts is + not supplied. It is recommended you always send include_rts=1 when using this API method. + in: query + name: count + required: false + type: integer + - description: |- + Returns results with an ID greater than (that is, more recent than) the specified ID. There are + limits to the number of Tweets which can be accessed through the API. If the limit of Tweets has + occured since the since_id, the since_id will be forced to the oldest ID available. + format: int64 + in: query + name: since_id + required: false + type: integer + - description: 'Returns results with an ID less than (that is, older than) or equal to the specified ID.' + format: int64 + in: query + name: max_id + required: false + type: integer + - description: |- + When set to either true, t or 1, each tweet returned in a timeline will include a user object + including only the status authors numerical ID. Omit this parameter to receive the complete user + object. + in: query + name: trim_user + required: false + type: string + - description: |- + This parameter enhances the contributors element of the status response to include the screen_name + of the contributor. By default only the user_id of the contributor is included. + in: query + name: contributor_details + required: false + type: string + - description: The entities node will be disincluded when set to false. + in: query + name: include_entities + required: false + type: boolean + /statuses/oembed.json: + get: + description: |- + Returns information allowing + the creation of an embedded representation of a Tweet on third party sites. See the oEmbed + specification (http://oembed.com) for information about the response format. Either the id or url + parameters must be specified in a request, it is not necessary to include both. While this endpoint + allows a bit of customization for the final appearance of the embedded Tweet, be aware that the + appearance of the rendered Tweet may change over time to be consistent with Twitter's Display + Guidelines (https://dev.twitter.com/terms/display-guidelines). Do not rely on any class or id + parameters to stay constant in the returned markup. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/statuses/oembed' + operationId: statuses.oembed + responses: + '200': + description: Successful Response + parameters: + - description: |- + The maximum width in pixels that the embed should be rendered at. This value is constrained to be + between 250 and 550 pixels. Note that Twitter does not support the oEmbed maxheight parameter. + Tweets are fundamentally text, and are therefore of unpredictable height that cannot be scaled like + an image or video. Relatedly, the oEmbed response will not provide a value for height. + Implementations that need consistent heights for Tweets should refer to the hide_thread and + hide_media parameters below. + in: query + name: maxwidth + required: false + type: integer + - description: |- + Specifies whether the embedded Tweet should automatically expand images which were uploaded via + POST statuses/update_with_media. When set to either true, t or + 1 images will not be expanded. Defaults to false. + in: query + name: hide_media + required: false + type: string + - description: |- + Specifies whether the embedded Tweet should automatically show the original message in the case + that the embedded Tweet is a reply. When set to either true, t or 1 the original Tweet will not be + shown. Defaults to false. + in: query + name: hide_thread + required: false + type: string + - description: |- + Specifies whether the embedded Tweet HTML should include a + 'script' element pointing to widgets.js. In cases where a page already includes widgets.js, setting + this + value to true will prevent a redundant script element from being included. When set to either true, + t or 1 the 'script'element will not be included in the embed HTML, meaning that pages must include a + reference to + widgets.js manually. Defaults to false. + in: query + name: omit_script + required: false + type: string + - description: |- + Specifies whether the embedded Tweet should be left aligned, right aligned, or centered in the + page. Valid values are left, right, center, and none. Defaults to none, meaning no alignment styles + are specified for the Tweet. + enum: + - left + - right + - center + - none + in: query + name: align + required: false + type: string + - description: |- + A value for the TWT related parameter, as described in Web Intents + (https://dev.twitter.com/docs/intents). This value will be forwarded to all Web Intents calls. + Example values: twitterapi, twittermedia, twitter. + in: query + name: related + required: false + type: string + - description: |- + Language code for the rendered embed. This will affect the text and localization of the rendered + HTML. Example value: fr + in: query + name: lang + required: false + type: string + '/statuses/retweet/{id}.json': + parameters: + - description: The numerical ID of the desired status. + in: path + name: id + required: true + type: string + - description: |- + When set to either true, t or 1, each tweet returned in a timeline will include a user object + including only the status authors numerical ID. Omit this parameter to receive the complete user + object. + in: query + name: trim_user + required: false + type: string + post: + description: |- + Retweets a tweet. + Returns + the original tweet with retweet details embedded. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/statuses/retweet/:id' + operationId: statusesretweetid + responses: + '200': + description: Successful Response + '/statuses/retweets/{id}.json': + get: + description: |- + Returns up to 100 of + the + first retweets of a given tweet. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/statuses/retweets/:id' + operationId: statuses.retweets + responses: + '200': + description: Successful Response + parameters: + - description: The numerical ID of the desired status. + in: path + name: id + required: true + type: string + - description: Specifies the number of records to retrieve. Must be less than or equal to 100. + in: query + name: count + required: false + type: string + - description: |- + When set to either true, t or 1, each tweet returned in a timeline will include a user object + including only the status authors numerical ID. Omit this parameter to receive the complete user + object. + in: query + name: trim_user + required: false + type: string + '/statuses/show/{id}.json': + get: + description: |- + Returns a single status, + specified by the id parameter below. The status's author will be returned inline. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/statuses/show/:id' + operationId: statuses.show + parameters: + - in: path + name: id + required: true + type: string + responses: + '200': + description: Successful Response + parameters: + - description: The numerical ID of the desired status. + in: query + name: id + required: true + type: string + - description: |- + When set to either true, t or 1, each tweet returned in a timeline will include a user object + including only the status authors numerical ID. Omit this parameter to receive the complete user + object. + in: query + name: trim_user + required: false + type: string + - description: |- + When set to either true, t or 1, any Tweets returned that have been retweeted by the authenticating + user will include an additional current_user_retweet node, containing the ID of the source status + for the retweet. + in: query + name: include_my_retweet + required: false + type: string + - description: The entities node will be disincluded when set to false. + in: query + name: include_entities + required: false + type: string + /statuses/update.json: + parameters: + - default: 'Posting from @apigee''s API test console. It''s like a command line for the Twitter API! #apitools' + description: |- + The text of your status update, typically up to 140 characters. URL encode as necessary. t.co link + short-url wrapping (https://dev.twitter.com/docs/tco-link-wrapper/faq) may effect character counts. + in: query + name: status + required: true + type: string + - description: |- + The ID of an existing status that the update is in reply to. Note: This parameter will be ignored + unless the author of the tweet this parameter references is mentioned within the status text. + Therefore, you must include @username, where username is the author of the referenced tweet, within + the update. + in: query + name: in_reply_to_status_id + required: false + type: string + - default: '37.426363' + description: |- + The latitude of the location this tweet refers to. This parameter will be ignored unless it is + inside the range -90.0 to +90.0 (North is positive) inclusive. It will also be ignored if there + isn't a corresponding long parameter. + in: query + name: lat + required: false + type: string + - default: '-122.141114' + description: |- + The longitude of the location this tweet refers to. The valid ranges for longitude is -180.0 to + +180.0 (East is positive) inclusive. This parameter will be ignored if outside that range, if it is + not a number, if geo_enabled is disabled, or if there not a corresponding lat parameter. + in: query + name: long + required: false + type: string + - description: |- + A place in the world. These IDs can be retrieved from GET geo/reverse_geocode + (https://dev.twitter.com/docs/api/1/get/geo/reverse_geocode). + in: query + name: place_id + required: false + type: string + - default: 'false' + description: Whether or not to put a pin on the exact coordinates a tweet has been sent from. + enum: + - 'false' + - 'true' + - '' + in: query + name: display_coordinates + required: false + type: string + - description: |- + When set to either true, t or 1, each tweet returned in a timeline will include a user object + including only the status authors numerical ID. Omit this parameter to receive the complete user + object. + in: query + name: trim_user + required: false + type: string + post: + description: |- + Updates the authenticating + user's status, also known as tweeting. To upload an image to accompany the tweet, use POST + statuses/update_with_media (https://dev.twitter.com/docs/api/1/post/statuses/update_with_media). For + each update attempt, the update text is compared with the authenticating user's recent tweets. Any + attempt that would result in duplication will be blocked, resulting in a 403 error. Therefore, a + user cannot submit the same status twice in a row. While not rate limited by the API a user is + limited in the number of tweets they can create at a time. If the number of updates posted by the + user reaches the current allowed limit this method will return an HTTP 403 error. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/statuses/update' + operationId: statuses.update + responses: + '200': + description: Successful Response + /statuses/update_with_media.json: + parameters: + - description: |- + The text of your status update. URL encode as necessary. t.co link wrapping + (https://dev.twitter.com/docs/tco-link-wrapper/faq) may affect character counts if the post contains + URLs. You must additionally account for the characters_reserved_per_media per uploaded media, + additionally accounting for space characters in between finalized URLs. Note: Request the GET + help/configuration (https://dev.twitter.com/docs/api/1.1/get/help/configuration) endpoint to get the + current characters_reserved_per_media and max_media_per_upload values. + in: query + name: status + required: true + type: string + - description: |- + Up to max_media_per_upload files may be specified in the request, each named media[]. Supported + image formats are PNG, JPG and GIF. Animated GIFs are not supported. Note: Request the GET + help/configuration (https://dev.twitter.com/docs/api/1.1/get/help/configuration) endpoint to get the + current max_media_per_upload and photo_size_limit values. + in: query + name: media + required: true + type: string + - description: Set to true for content which may not be suitable for every audience. + in: query + name: possibly_sensitive + required: false + type: string + - description: |- + The ID of an existing status that the update is in reply to. Note: This parameter will be ignored + unless the author of the tweet this parameter references is mentioned within the status text. + Therefore, you must include @username, where username is the author of the referenced tweet, within + the update. + in: query + name: in_reply_to_status_id + required: false + type: string + - description: |- + The latitude of the location this tweet refers to. This parameter will be ignored unless it is + inside the range -90.0 to +90.0 (North is positive) inclusive. It will also be ignored if there + isn't a corresponding long parameter. Example value: 37.7821120598956. + in: query + name: lat + required: false + type: string + - description: |- + The longitude of the location this tweet refers to. The valid ranges for longitude is -180.0 to + +180.0 (East is positive) inclusive. This parameter will be ignored if outside that range, not a + number, geo_enabled is disabled, or if there not a corresponding lat parameter. Example value: + -122.400612831116. + in: query + name: long + required: false + type: string + - description: |- + A place in the world identified by a Twitter place ID. Place IDs can be retrieved from + geo/reverse_geocode. + in: query + name: place_id + required: false + type: string + - description: Whether or not to put a pin on the exact coordinates a tweet has been sent from. + in: query + name: display_coordinates + required: false + type: string + post: + description: |- + Updates the + authenticating user's status and attaches media for upload. Unlike POST statuses/update + (https://dev.twitter.com/docs/api/1.1/post/statuses/update), this method expects raw multipart data. + Your POST request's Content-Type should be set to multipart/form-data with the media[] parameter. + The Tweet text will be rewritten to include the media URL(s), which will reduce the number of + characters allowed in the Tweet text. If the URL(s) cannot be appended without text truncation, the + tweet will be rejected and this method will return an HTTP 403 error. Important: Make sure that + you're using upload.twitter.com as your host while posting statuses with media. It is strongly + recommended to use SSL with this method. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media' + operationId: statuses.update_with_media + parameters: + - default: multipart/form-data + description: Content type. + in: header + name: Content-Type + required: true + type: string + responses: + '200': + description: Successful Response + /statuses/user_timeline.json: + get: + description: |- + Returns the 20 most + recent statuses posted by the authenticating user. It is also possible to request another user's + timeline by using the screen_name or user_id parameter. The other users timeline will only be + visible if they are not protected, or if the authenticating user's follow request was accepted by + the protected user. The timeline returned is the equivalent of the one seen when you view a user's + profile on twitter.com. This method can only return up to 3,200 of a user's most recent statuses. + Native retweets of other statuses by the user is included in this total, regardless of whether + include_rts is specified when requesting this resource. This method will not include retweets in the + XML and JSON responses unless the include_rts parameter is set. The RSS and Atom responses will + always include retweets as statuses prefixed with RT, regardless of provided parameters. Always + specify either an user_id or screen_name when requesting a user timeline. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline' + operationId: statuses.user_timeline + responses: + '200': + description: Successful Response + parameters: + - description: |- + Specifies the number of tweets to try and retrieve, up to a maximum of 200. The value of count is + best thought of as a limit to the number of tweets to return because suspended or deleted content is + removed after the count has been applied. We include retweets in the count, even if include_rts is + not supplied. It is recommended you always send include_rts=1 when using this API method. + in: query + name: count + required: false + type: integer + - description: |- + Returns results with an ID greater than (that is, more recent than) the specified ID. There are + limits to the number of Tweets which can be accessed through the API. If the limit of Tweets has + occured since the since_id, the since_id will be forced to the oldest ID available. + format: int64 + in: query + name: since_id + required: false + type: integer + - description: 'Returns results with an ID less than (that is, older than) or equal to the specified ID.' + format: int64 + in: query + name: max_id + required: false + type: integer + - description: |- + When set to either true, t or 1, each tweet returned in a timeline will include a user object + including only the status authors numerical ID. Omit this parameter to receive the complete user + object. + in: query + name: trim_user + required: false + type: string + - description: |- + This parameter will prevent replies from appearing in the returned timeline. Using exclude_replies + with the count parameter will mean you will receive up-to count tweets Ãĸâ‚Ŧ” this is because the count + parameter retrieves that many tweets before filtering out retweets and replies. This parameter is + only supported for JSON and XML responses. + in: query + name: exclude_replies + required: false + type: boolean + - description: |- + This parameter enhances the contributors element of the status response to include the screen_name + of the contributor. By default only the user_id of the contributor is included. + in: query + name: contributor_details + required: false + type: boolean + - description: |- + When set to false, the timeline will strip any native retweets (though they will still count toward + both the maximal length of the timeline and the slice selected by the count parameter). Note: If + you're using the trim_user parameter in conjunction with include_rts, the retweets will still + contain a full user object. + in: query + name: include_rts + required: false + type: boolean + /trends/available.json: + get: + description: |- + Returns the locations that + Twitter has trending topic information for. + + The response is an array of "locations" that encode the location's WOEID and some other + human-readable information such as a canonical name and country the location belongs in. + + A WOEID is a Yahoo! Where On Earth ID. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/trends/available' + operationId: trends.available + responses: + '200': + description: Successful Response + parameters: [] + /trends/closest.json: + get: + description: |- + Returns the locations that + Twitter has trending topic information for, closest to a specified location. + + The response is an array of "locations" that encode the location's WOEID and some other + human-readable information such as a canonical name and country the location belongs in. + + A WOEID is a Yahoo! Where On Earth ID. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/trends/closest' + operationId: trends.closest + responses: + '200': + description: Successful Response + parameters: + - description: |- + If provided with a long parameter the available trend locations will be sorted by distance, nearest + to furthest, to the co-ordinate pair. The valid ranges for longitude is -180.0 to +180.0 (West is + negative, East is positive) inclusive. + + Example Values: 37.781157 + in: query + name: lat + required: false + type: string + - description: |- + If provided with a lat parameter the available trend locations will be sorted by distance, nearest + to furthest, to the co-ordinate pair. The valid ranges for longitude is -180.0 to +180.0 (West is + negative, East is positive) inclusive. + + Example Values: -122.400612831116 + in: query + name: long + required: false + type: string + /trends/place.json: + get: + description: |- + Returns the top 10 trending + topics for a specific WOEID, if trending information is available for it. + + The response is an array of "trend" objects that encode the name of the trending topic, the query + parameter that can be used to search for the topic on Twitter Search, and the Twitter Search URL. + + This information is cached for 5 minutes. Requesting more frequently than that will not return any + more data, and will count against your rate limit usage. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/trends/place' + operationId: trends.place + responses: + '200': + description: Successful Response + parameters: + - description: |- + The Yahoo! Where On Earth ID of the location to return trending information for. Global information + is available by using 1 as the WOEID. + in: query + name: id + required: true + type: string + - description: Setting this equal to hashtags will remove all hashtags from the trends list. + in: query + name: exclude + required: false + type: string + /users/contributees.json: + get: + description: |- + Returns a collection of + users that the specified user can contribute to. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/users/contributees' + operationId: users.contributees + responses: + '200': + description: Successful Response + parameters: + - description: 'The entities node will be disincluded when set to false. Example Values: false' + in: query + name: include_entities + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + /users/contributors.json: + get: + description: |- + Returns a collection of + users who can contribute to the specified account. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/users/contributors' + operationId: users.contributors + responses: + '200': + description: Successful Response + parameters: + - description: 'The entities node will be disincluded when set to false. Example Values: false' + in: query + name: include_entities + required: false + type: string + - description: 'When set to either true, t or 1 statuses will not be included in the returned user objects.' + in: query + name: skip_status + required: false + type: string + /users/lookup.json: + get: + description: |- + Returns fully-hydrated user + objects for up to 100 users per request, as specified by comma-separated values passed to the + user_id and/or screen_name parameters. + + This method is especially useful when used in conjunction with collections of user IDs returned from + GET friends/ids and GET followers/ids. + + GET users/show is used to retrieve a single user object. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/users/lookup' + operationId: users.lookup + responses: + '200': + description: Successful Response + parameters: + - description: |- + A comma separated list of screen names, up to 100 are allowed in a single request. You are strongly + encouraged to use a POST for larger (up to 100 screen names) requests. + + Example Values: twitterapi,twitter + in: query + name: screen_name + required: false + type: string + - description: |- + A comma separated list of user IDs, up to 100 are allowed in a single request. You are strongly + encouraged to use a POST for larger requests. + + Example Values: 783214,6253282 + in: query + name: user_id + required: false + type: string + - description: |- + The entities node that may appear within embedded statuses will be disincluded when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + /users/report_spam.json: + parameters: [] + post: + description: |- + The user + specified in the id is blocked by the authenticated user and reported as a spammer. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/post/report_spam' + operationId: users.report_spam + responses: + '200': + description: Successful Response + /users/search.json: + get: + description: |- + Provides a simple, + relevance-based search interface to public user accounts on Twitter. Try querying by topical + interest, full name, company name, location, or other criteria. Exact match searches are not + supported. + + Only the first 1,000 matching results are available. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/users/search' + operationId: users.search + responses: + '200': + description: Successful Response + parameters: + - description: |- + The search query to run against people search. + + Example Values: Twitter%20API + in: query + name: q + required: true + type: string + - description: |- + Specifies the page of results to retrieve. + + Example Values: 3 + in: query + name: page + required: false + type: string + - description: |- + The number of potential user results to retrieve per page. This value has a maximum of 20. + + Example Values: 5 + in: query + name: count + required: false + type: string + - description: |- + The entities node will be disincluded when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + /users/show.json: + get: + description: |- + Returns a variety of information + about the user specified by the required user_id or screen_name parameter. The author's most recent + Tweet will be returned inline when possible. + + GET users/lookup is used to retrieve a bulk collection of user objects. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/users/show' + operationId: users.show + responses: + '200': + description: Successful Response + parameters: + - description: |- + The screen name of the user for whom to return results for. Either a id or screen_name is required + for this method. + + Example Values: noradio + in: query + name: screen_name + required: true + type: string + - description: |- + The ID of the user for whom to return results for. Either an id or screen_name is required for this + method. + + Example Values: 12345 + in: query + name: user_id + required: true + type: string + - description: |- + The entities node will be disincluded when set to false. + + Example Values: false + in: query + name: include_entities + required: false + type: string + /users/suggestions.json: + get: + description: |- + Access to Twitter's + suggested user list. This returns the list of suggested user categories. The category can be used in + GET users/suggestions/:slug to get the users in that category. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/users/suggestions' + operationId: users.suggestions + responses: + '200': + description: Successful Response + parameters: + - description: |- + Restricts the suggested categories to the requested language. The language must be specified by the + appropriate two letter ISO 639-1 representation. Currently supported languages are provided by the + GET help/languages API request. Unsupported language codes will receive English (en) results. If you + use lang in this request, ensure you also include it when requesting the GET users/suggestions/:slug + list. + in: query + name: lang + required: false + type: string + '/users/suggestions/{slug}.json': + get: + description: |- + Access the users in + a given category of the Twitter suggested user list. It is recommended that applications cache this + data for no more than one hour. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/users/suggestions/%3Aslug' + operationId: users.suggestions.slug + responses: + '200': + description: Successful Response + parameters: + - description: |- + The short name of list or a category + + Example Values: twitter + in: path + name: slug + required: true + type: string + - description: |- + Restricts the suggested categories to the requested language. The language must be specified by the + appropriate two letter ISO 639-1 representation. Currently supported languages are provided by the + GET help/languages API request. Unsupported language codes will receive English (en) results. If you + use lang in this request, ensure you also include it when requesting the GET users/suggestions/:slug + list. + in: query + name: lang + required: false + type: string + '/users/suggestions/{slug}/members.json': + get: + description: |- + Access the + users in a given category of the Twitter suggested user list and return their most recent status if + they are not a protected user. + externalDocs: + url: 'https://dev.twitter.com/docs/api/1.1/get/users/suggestions/%3Aslug/members' + operationId: users.suggestionsslugmembers + responses: + '200': + description: Successful Response + parameters: + - description: |- + The short name of list or a category + + Example Values: twitter + in: path + name: slug + required: true + type: string \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue_911.yaml b/modules/swagger-parser/src/test/resources/issue_911.yaml new file mode 100644 index 0000000000..8d05494e22 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue_911.yaml @@ -0,0 +1,44 @@ +swagger: 2.0 +info: + description: >- + This is a sample server Petstore server. You can find out more about + Swagger at http://swagger.io or on + irc.freenode.net, #swagger. For this sample, you can use the api key + "special-key" to test the authorization filters + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + name: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +externalDocs: + description: Find more info here + url: 'https://swagger.io' +tags: +- name: pet + description: Pet Operations + externalDocs: + url: 'http://swagger.io' +- name: user + description: All about the Users +paths: + '/pet/{petId}/{pathParamNotDefined}': + get: + tags: + - pet + summary: Find pet by ID + description: >- + Returns a pet when ID < 10. ID > 10 or nonintegers will simulate API + error conditions + operationId: getPetById + responses: + 200: + description: 200 ok + parameters: + - name: petId + in: path + description: ID of pet that needs to be fetched + required: true + type: integer \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/issue_998.yaml b/modules/swagger-parser/src/test/resources/issue_998.yaml new file mode 100755 index 0000000000..93503426f2 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue_998.yaml @@ -0,0 +1,602 @@ +&id001 +*id001: poop +r: &id200 + a: + a: &id198 + a: &id196 + a: &id194 + a: &id192 + a: &id190 + a: &id188 + a: &id186 + a: &id184 + a: &id182 + a: &id180 + a: &id178 + a: &id176 + a: &id174 + a: &id172 + a: &id170 + a: &id168 + a: &id166 + a: &id164 + a: &id162 + a: &id160 + a: &id158 + a: &id156 + a: &id154 + a: &id152 + a: &id150 + a: &id148 + a: &id146 + a: &id144 + a: &id142 + a: &id140 + a: &id138 + a: &id136 + a: &id134 + a: &id132 + a: &id130 + a: &id128 + a: &id126 + a: &id124 + a: &id122 + a: &id120 + a: &id118 + a: &id116 + a: &id114 + a: &id112 + a: &id110 + a: &id108 + a: &id106 + a: &id104 + a: &id102 + a: &id100 + a: &id098 + a: &id096 + a: &id094 + a: &id092 + a: &id090 + a: &id088 + a: &id086 + a: &id084 + a: &id082 + a: &id080 + a: &id078 + a: &id076 + a: &id074 + a: &id072 + a: &id070 + a: &id068 + a: &id066 + a: &id064 + a: &id062 + a: &id060 + a: &id058 + a: &id056 + a: &id054 + a: &id052 + a: &id050 + a: &id048 + a: &id046 + a: &id044 + a: &id042 + a: &id040 + a: &id038 + a: &id036 + a: &id034 + a: &id032 + a: &id030 + a: &id028 + a: &id026 + a: &id024 + a: &id022 + a: &id020 + a: &id018 + a: &id016 + a: &id014 + a: &id012 + a: &id010 + a: &id008 + a: &id006 + a: &id004 + a: &id002 { + foo: '1'} + b: &id003 { + bar: '2'} + foo: '1' + b: &id005 + a: *id002 + bar: '2' + b: *id003 + foo: '1' + b: &id007 + a: *id004 + bar: '2' + b: *id005 + foo: '1' + b: &id009 + a: *id006 + bar: '2' + b: *id007 + foo: '1' + b: &id011 + a: *id008 + bar: '2' + b: *id009 + foo: '1' + b: &id013 + a: *id010 + bar: '2' + b: *id011 + foo: '1' + b: &id015 + a: *id012 + bar: '2' + b: *id013 + foo: '1' + b: &id017 + a: *id014 + bar: '2' + b: *id015 + foo: '1' + b: &id019 + a: *id016 + bar: '2' + b: *id017 + foo: '1' + b: &id021 + a: *id018 + bar: '2' + b: *id019 + foo: '1' + b: &id023 + a: *id020 + bar: '2' + b: *id021 + foo: '1' + b: &id025 + a: *id022 + bar: '2' + b: *id023 + foo: '1' + b: &id027 + a: *id024 + bar: '2' + b: *id025 + foo: '1' + b: &id029 + a: *id026 + bar: '2' + b: *id027 + foo: '1' + b: &id031 + a: *id028 + bar: '2' + b: *id029 + foo: '1' + b: &id033 + a: *id030 + bar: '2' + b: *id031 + foo: '1' + b: &id035 + a: *id032 + bar: '2' + b: *id033 + foo: '1' + b: &id037 + a: *id034 + bar: '2' + b: *id035 + foo: '1' + b: &id039 + a: *id036 + bar: '2' + b: *id037 + foo: '1' + b: &id041 + a: *id038 + bar: '2' + b: *id039 + foo: '1' + b: &id043 + a: *id040 + bar: '2' + b: *id041 + foo: '1' + b: &id045 + a: *id042 + bar: '2' + b: *id043 + foo: '1' + b: &id047 + a: *id044 + bar: '2' + b: *id045 + foo: '1' + b: &id049 + a: *id046 + bar: '2' + b: *id047 + foo: '1' + b: &id051 + a: *id048 + bar: '2' + b: *id049 + foo: '1' + b: &id053 + a: *id050 + bar: '2' + b: *id051 + foo: '1' + b: &id055 + a: *id052 + bar: '2' + b: *id053 + foo: '1' + b: &id057 + a: *id054 + bar: '2' + b: *id055 + foo: '1' + b: &id059 + a: *id056 + bar: '2' + b: *id057 + foo: '1' + b: &id061 + a: *id058 + bar: '2' + b: *id059 + foo: '1' + b: &id063 + a: *id060 + bar: '2' + b: *id061 + foo: '1' + b: &id065 + a: *id062 + bar: '2' + b: *id063 + foo: '1' + b: &id067 + a: *id064 + bar: '2' + b: *id065 + foo: '1' + b: &id069 + a: *id066 + bar: '2' + b: *id067 + foo: '1' + b: &id071 + a: *id068 + bar: '2' + b: *id069 + foo: '1' + b: &id073 + a: *id070 + bar: '2' + b: *id071 + foo: '1' + b: &id075 + a: *id072 + bar: '2' + b: *id073 + foo: '1' + b: &id077 + a: *id074 + bar: '2' + b: *id075 + foo: '1' + b: &id079 + a: *id076 + bar: '2' + b: *id077 + foo: '1' + b: &id081 + a: *id078 + bar: '2' + b: *id079 + foo: '1' + b: &id083 + a: *id080 + bar: '2' + b: *id081 + foo: '1' + b: &id085 + a: *id082 + bar: '2' + b: *id083 + foo: '1' + b: &id087 + a: *id084 + bar: '2' + b: *id085 + foo: '1' + b: &id089 + a: *id086 + bar: '2' + b: *id087 + foo: '1' + b: &id091 + a: *id088 + bar: '2' + b: *id089 + foo: '1' + b: &id093 + a: *id090 + bar: '2' + b: *id091 + foo: '1' + b: &id095 + a: *id092 + bar: '2' + b: *id093 + foo: '1' + b: &id097 + a: *id094 + bar: '2' + b: *id095 + foo: '1' + b: &id099 + a: *id096 + bar: '2' + b: *id097 + foo: '1' + b: &id101 + a: *id098 + bar: '2' + b: *id099 + foo: '1' + b: &id103 + a: *id100 + bar: '2' + b: *id101 + foo: '1' + b: &id105 + a: *id102 + bar: '2' + b: *id103 + foo: '1' + b: &id107 + a: *id104 + bar: '2' + b: *id105 + foo: '1' + b: &id109 + a: *id106 + bar: '2' + b: *id107 + foo: '1' + b: &id111 + a: *id108 + bar: '2' + b: *id109 + foo: '1' + b: &id113 + a: *id110 + bar: '2' + b: *id111 + foo: '1' + b: &id115 + a: *id112 + bar: '2' + b: *id113 + foo: '1' + b: &id117 + a: *id114 + bar: '2' + b: *id115 + foo: '1' + b: &id119 + a: *id116 + bar: '2' + b: *id117 + foo: '1' + b: &id121 + a: *id118 + bar: '2' + b: *id119 + foo: '1' + b: &id123 + a: *id120 + bar: '2' + b: *id121 + foo: '1' + b: &id125 + a: *id122 + bar: '2' + b: *id123 + foo: '1' + b: &id127 + a: *id124 + bar: '2' + b: *id125 + foo: '1' + b: &id129 + a: *id126 + bar: '2' + b: *id127 + foo: '1' + b: &id131 + a: *id128 + bar: '2' + b: *id129 + foo: '1' + b: &id133 + a: *id130 + bar: '2' + b: *id131 + foo: '1' + b: &id135 + a: *id132 + bar: '2' + b: *id133 + foo: '1' + b: &id137 + a: *id134 + bar: '2' + b: *id135 + foo: '1' + b: &id139 + a: *id136 + bar: '2' + b: *id137 + foo: '1' + b: &id141 + a: *id138 + bar: '2' + b: *id139 + foo: '1' + b: &id143 + a: *id140 + bar: '2' + b: *id141 + foo: '1' + b: &id145 + a: *id142 + bar: '2' + b: *id143 + foo: '1' + b: &id147 + a: *id144 + bar: '2' + b: *id145 + foo: '1' + b: &id149 + a: *id146 + bar: '2' + b: *id147 + foo: '1' + b: &id151 + a: *id148 + bar: '2' + b: *id149 + foo: '1' + b: &id153 + a: *id150 + bar: '2' + b: *id151 + foo: '1' + b: &id155 + a: *id152 + bar: '2' + b: *id153 + foo: '1' + b: &id157 + a: *id154 + bar: '2' + b: *id155 + foo: '1' + b: &id159 + a: *id156 + bar: '2' + b: *id157 + foo: '1' + b: &id161 + a: *id158 + bar: '2' + b: *id159 + foo: '1' + b: &id163 + a: *id160 + bar: '2' + b: *id161 + foo: '1' + b: &id165 + a: *id162 + bar: '2' + b: *id163 + foo: '1' + b: &id167 + a: *id164 + bar: '2' + b: *id165 + foo: '1' + b: &id169 + a: *id166 + bar: '2' + b: *id167 + foo: '1' + b: &id171 + a: *id168 + bar: '2' + b: *id169 + foo: '1' + b: &id173 + a: *id170 + bar: '2' + b: *id171 + foo: '1' + b: &id175 + a: *id172 + bar: '2' + b: *id173 + foo: '1' + b: &id177 + a: *id174 + bar: '2' + b: *id175 + foo: '1' + b: &id179 + a: *id176 + bar: '2' + b: *id177 + foo: '1' + b: &id181 + a: *id178 + bar: '2' + b: *id179 + foo: '1' + b: &id183 + a: *id180 + bar: '2' + b: *id181 + foo: '1' + b: &id185 + a: *id182 + bar: '2' + b: *id183 + foo: '1' + b: &id187 + a: *id184 + bar: '2' + b: *id185 + foo: '1' + b: &id189 + a: *id186 + bar: '2' + b: *id187 + foo: '1' + b: &id191 + a: *id188 + bar: '2' + b: *id189 + foo: '1' + b: &id193 + a: *id190 + bar: '2' + b: *id191 + foo: '1' + b: &id195 + a: *id192 + bar: '2' + b: *id193 + foo: '1' + b: &id197 + a: *id194 + bar: '2' + b: *id195 + foo: '1' + b: &id199 + a: *id196 + bar: '2' + b: *id197 + foo: '1' + b: + a: *id198 + bar: '2' + b: *id199 +j: *id200 \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/nested-external-response-references/sub-folder/common-country.yaml b/modules/swagger-parser/src/test/resources/nested-external-response-references/sub-folder/common-country.yaml new file mode 100644 index 0000000000..6a7a908889 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/nested-external-response-references/sub-folder/common-country.yaml @@ -0,0 +1,18 @@ +swagger: "2.0" +info: + title: Sample API + description: API description in Markdown. + version: 1.0.0 +host: api.example.com +basePath: /v1 +schemes: + - https +paths: {} + +definitions: + + Country: + type: object + properties: + name: + type: string \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/nested-external-response-references/sub-folder/common-responses.yaml b/modules/swagger-parser/src/test/resources/nested-external-response-references/sub-folder/common-responses.yaml new file mode 100644 index 0000000000..2f5b03308f --- /dev/null +++ b/modules/swagger-parser/src/test/resources/nested-external-response-references/sub-folder/common-responses.yaml @@ -0,0 +1,27 @@ +swagger: "2.0" +info: + title: Sample API + description: API description in Markdown. + version: 1.0.0 +host: api.example.com +basePath: /v1 +schemes: + - https +paths: {} + +responses: + Ok200: + description: OK + schema: + $ref: '../swagger-root.yaml#/definitions/User' + + Ok201: + description: OK + schema: + $ref: '#/definitions/User' + +definitions: + User: + $ref: '../sub-folder/sub-folder2/common-address.yaml#/definitions/Address' +#Still a BUG: Should take into account that the like is relative to this file! +# $ref: 'sub-folder2/common-address.yaml#/definitions/Address' \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/nested-external-response-references/sub-folder/sub-folder2/common-address.yaml b/modules/swagger-parser/src/test/resources/nested-external-response-references/sub-folder/sub-folder2/common-address.yaml new file mode 100644 index 0000000000..7f1179ef7e --- /dev/null +++ b/modules/swagger-parser/src/test/resources/nested-external-response-references/sub-folder/sub-folder2/common-address.yaml @@ -0,0 +1,20 @@ +swagger: "2.0" +info: + title: Sample API + description: API description in Markdown. + version: 1.0.0 +host: api.example.com +basePath: /v1 +schemes: + - https +paths: {} + +definitions: + + Address: + type: object + properties: + postal: + type: string + country: + $ref: '../common-country.yaml#/definitions/Country' diff --git a/modules/swagger-parser/src/test/resources/nested-external-response-references/swagger-root.yaml b/modules/swagger-parser/src/test/resources/nested-external-response-references/swagger-root.yaml new file mode 100644 index 0000000000..e87ff1efc7 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/nested-external-response-references/swagger-root.yaml @@ -0,0 +1,31 @@ +swagger: "2.0" +info: + title: Sample API + description: API description in Markdown. + version: 1.0.0 +host: api.example.com +basePath: /v1 +schemes: + - https +paths: + /users: + get: + summary: Returns a list of users. + description: Optional extended description in Markdown. + produces: + - application/json + responses: + 200: + $ref: 'sub-folder/common-responses.yaml#/responses/Ok200' + 201: + $ref: './sub-folder/common-responses.yaml#/responses/Ok201' + +definitions: + UserX: + type: object + properties: + address: + $ref: './sub-folder/sub-folder2/common-address.yaml#/definitions/Address' + + User: + $ref: './sub-folder/sub-folder2/common-address.yaml#/definitions/Address' \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/nested-file-references/common/eventsWithPagingInSubdir.yaml b/modules/swagger-parser/src/test/resources/nested-file-references/common/eventsWithPagingInSubdir.yaml new file mode 100644 index 0000000000..5e23406f2b --- /dev/null +++ b/modules/swagger-parser/src/test/resources/nested-file-references/common/eventsWithPagingInSubdir.yaml @@ -0,0 +1,12 @@ +get: + description: A list of events + operationId: getEvents + responses: + 200: + description: OK + schema: + type: object + properties: + paging: +# type: string + $ref: 'paging2.yaml#/Paging2' diff --git a/modules/swagger-parser/src/test/resources/nested-file-references/common/paging2.yaml b/modules/swagger-parser/src/test/resources/nested-file-references/common/paging2.yaml index 41d22e8ed5..59260b0ec3 100644 --- a/modules/swagger-parser/src/test/resources/nested-file-references/common/paging2.yaml +++ b/modules/swagger-parser/src/test/resources/nested-file-references/common/paging2.yaml @@ -3,4 +3,4 @@ Paging2: total_items: type: integer foobar: - $ref: '../common2/bar.yaml#/Foobar' + $ref: './common2/bar.yaml#/Foobar' diff --git a/modules/swagger-parser/src/test/resources/nested-file-references/issue-1223.yaml b/modules/swagger-parser/src/test/resources/nested-file-references/issue-1223.yaml new file mode 100644 index 0000000000..5bba8b3f41 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/nested-file-references/issue-1223.yaml @@ -0,0 +1,22 @@ +swagger: '2.0' +info: + title: Test API + version: '1' +host: example.com +basePath: /api/v1 +schemes: + - https +consumes: + - application/json; charset=utf-8 +produces: + - application/json; charset=utf-8 + +paths: + /events: + $ref: './common/eventsWithPagingInSubdir.yaml' + +definitions: + StatusResponse: + properties: + http_code: + type: integer diff --git a/modules/swagger-parser/src/test/resources/nested-file-references/issue-421.yaml b/modules/swagger-parser/src/test/resources/nested-file-references/issue-421.yaml index ccf0cbde16..c67301a01d 100644 --- a/modules/swagger-parser/src/test/resources/nested-file-references/issue-421.yaml +++ b/modules/swagger-parser/src/test/resources/nested-file-references/issue-421.yaml @@ -66,12 +66,12 @@ paths: description: order placed for purchasing the pet required: true schema: - $ref: 'http://petstore.swagger.io/v2/swagger.json#/definitions/Order' + $ref: 'https://petstore.swagger.io/v2/swagger.json#/definitions/Order' responses: '200': description: successful operation schema: - $ref: 'http://petstore.swagger.io/v2/swagger.json#/definitions/Order' + $ref: 'https://petstore.swagger.io/v2/swagger.json#/definitions/Order' '400': description: Invalid Order diff --git a/modules/swagger-parser/src/test/resources/number_attributes.yaml b/modules/swagger-parser/src/test/resources/number_attributes.yaml new file mode 100644 index 0000000000..bd9b1a2626 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/number_attributes.yaml @@ -0,0 +1,45 @@ +--- +swagger: '2.0' +info: + version: 1.0.0 + title: Pets Store +paths: {} +definitions: + + NumberType: + type: number + enum: + - 1.0 + - 2.0 + default: 1.0 + minimum: 1.0 + maximum: 2.0 + + NumberDoubleType: + type: number + format: double + enum: + - 1.0 + - 2.0 + default: 1.0 + minimum: 1.0 + maximum: 2.0 + + IntegerType: + type: integer + enum: + - 1 + - 2 + default: 1 + minimum: 1 + maximum: 2 + + IntegerInt32Type: + type: integer + format: int32 + enum: + - 1 + - 2 + default: 1 + minimum: 1 + maximum: 2 diff --git a/modules/swagger-parser/src/test/resources/parameters-external/data-plane/Common/Parameters.json b/modules/swagger-parser/src/test/resources/parameters-external/data-plane/Common/Parameters.json new file mode 100644 index 0000000000..9e0a193b6e --- /dev/null +++ b/modules/swagger-parser/src/test/resources/parameters-external/data-plane/Common/Parameters.json @@ -0,0 +1,57 @@ +{ + "swagger": "2.0", + "info": { + "version": "2017-08-30", + "title": "Common Referenced Parameters File", + "description": "File containing commonly referenced parameters." + }, + "paths": {}, + "parameters": { + "GlobalEndpoint": { + "name": "Endpoint", + "description": "Supported Cognitive Services endpoints (protocol and hostname, for example: \"https://westus.api.cognitive.microsoft.com\", \"https://api.cognitive.microsoft.com\").", + "x-ms-parameter-location": "client", + "required": true, + "type": "string", + "in": "path", + "x-ms-skip-url-encoding": true, + "default": "https://api.cognitive.microsoft.com" + }, + "ImageStream": { + "name": "Image", + "in": "body", + "required": true, + "x-ms-parameter-location": "method", + "description": "An image stream.", + "schema": { + "type": "object", + "format": "file" + } + }, + "ImageUrl": { + "name": "ImageUrl", + "in": "body", + "required": true, + "x-ms-parameter-location": "method", + "x-ms-client-flatten": true, + "description": "A JSON document with a URL pointing to the image that is to be analyzed.", + "schema": { + "$ref": "#/definitions/ImageUrl" + } + } + }, + "definitions": { + "ImageUrl": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "description": "Publicly reachable URL of an image", + "type": "string" + } + } + } + } +} diff --git a/modules/swagger-parser/src/test/resources/parameters-external/data-plane/ComputerVision/stable/v1.0/ComputerVision.json b/modules/swagger-parser/src/test/resources/parameters-external/data-plane/ComputerVision/stable/v1.0/ComputerVision.json new file mode 100644 index 0000000000..93747df8e8 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/parameters-external/data-plane/ComputerVision/stable/v1.0/ComputerVision.json @@ -0,0 +1,1487 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0", + "title": "Computer Vision API", + "description": "The Computer Vision API provides state-of-the-art algorithms to process images and return information. For example, it can be used to determine if an image contains mature content, or it can be used to find all the faces in an image. It also has other features like estimating dominant and accent colors, categorizing the content of images, and describing an image with complete English sentences. Additionally, it can also intelligently generate images thumbnails for displaying large images effectively." + }, + "securityDefinitions": { + "apim_key": { + "type": "apiKey", + "name": "Ocp-Apim-Subscription-Key", + "in": "header" + } + }, + "security": [ + { + "apim_key": [] + } + ], + "x-ms-parameterized-host": { + "hostTemplate": "{AzureRegion}.api.cognitive.microsoft.com", + "parameters": [ + { + "$ref": "../../../Common/ExtendedRegions.json#/parameters/AzureRegion" + } + ] + }, + "basePath": "/vision/v1.0", + "schemes": [ + "https" + ], + "paths": { + "/models": { + "get": { + "description": "This operation returns the list of domain-specific models that are supported by the Computer Vision API. Currently, the API only supports one domain-specific model: a celebrity recognizer. A successful response will be returned in JSON. If the request failed, the response will contain an error code and a message to help understand what went wrong.", + "operationId": "ListModels", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "List of available domain models.", + "schema": { + "$ref": "#/definitions/ListModelsResult" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful List Domains request": { + "$ref": "./examples/SuccessfulListDomainModels.json" + } + } + } + }, + "/analyze": { + "post": { + "description": "This operation extracts a rich set of visual features based on the image content. Two input methods are supported -- (1) Uploading an image or (2) specifying an image URL. Within your request, there is an optional parameter to allow you to choose which features to return. By default, image categories are returned in the response.", + "operationId": "AnalyzeImage", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "$ref": "#/parameters/VisualFeatures" + }, + { + "name": "details", + "in": "query", + "description": "A string indicating which domain-specific details to return. Multiple values should be comma-separated. Valid visual feature types include:Celebrities - identifies celebrities if detected in the image.", + "type": "array", + "required": false, + "collectionFormat": "csv", + "items": { + "type": "string", + "x-nullable": false, + "x-ms-enum": { + "name": "Details", + "modelAsString": false + }, + "enum": [ + "Celebrities", + "Landmarks" + ] + } + }, + { + "$ref": "#/parameters/ServiceLanguage" + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageUrl" + } + ], + "responses": { + "200": { + "description": "The response include the extracted features in JSON format.Here is the definitions for enumeration typesClipartTypeNon-clipart = 0, ambiguous = 1, normal-clipart = 2, good-clipart = 3.LineDrawingTypeNon-LineDrawing = 0,LineDrawing = 1.", + "schema": { + "$ref": "#/definitions/ImageAnalysis" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Analyze with Url request": { + "$ref": "./examples/SuccessfulAnalyzeWithUrl.json" + } + } + } + }, + "/generateThumbnail": { + "post": { + "description": "This operation generates a thumbnail image with the user-specified width and height. By default, the service analyzes the image, identifies the region of interest (ROI), and generates smart cropping coordinates based on the ROI. Smart cropping helps when you specify an aspect ratio that differs from that of the input image. A successful response contains the thumbnail image binary. If the request failed, the response contains an error code and a message to help determine what went wrong.", + "operationId": "GenerateThumbnail", + "consumes": [ + "application/json" + ], + "produces": [ + "application/octet-stream" + ], + "parameters": [ + { + "name": "width", + "type": "integer", + "in": "query", + "required": true, + "minimum": 1, + "maximum": 1023, + "description": "Width of the thumbnail. It must be between 1 and 1024. Recommended minimum of 50." + }, + { + "name": "height", + "type": "integer", + "in": "query", + "required": true, + "minimum": 1, + "maximum": 1023, + "description": "Height of the thumbnail. It must be between 1 and 1024. Recommended minimum of 50." + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageUrl" + }, + { + "name": "smartCropping", + "type": "boolean", + "in": "query", + "required": false, + "default": false, + "description": "Boolean flag for enabling smart cropping." + } + ], + "responses": { + "200": { + "description": "The generated thumbnail in binary format.", + "schema": { + "type": "file" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Generate Thumbnail request": { + "$ref": "./examples/SuccessfulGenerateThumbnailWithUrl.json" + } + } + } + }, + "/ocr": { + "post": { + "description": "Optical Character Recognition (OCR) detects printed text in an image and extracts the recognized characters into a machine-usable character stream. Upon success, the OCR results will be returned. Upon failure, the error code together with an error message will be returned. The error code can be one of InvalidImageUrl, InvalidImageFormat, InvalidImageSize, NotSupportedImage, NotSupportedLanguage, or InternalServerError.", + "operationId": "RecognizePrintedText", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "$ref": "#/parameters/DetectOrientation" + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageUrl" + }, + { + "$ref": "#/parameters/OcrLanguage" + } + ], + "responses": { + "200": { + "description": "The OCR results in the hierarchy of region/line/word. The results include text, bounding box for regions, lines and words.textAngleThe angle, in degrees, of the detected text with respect to the closest horizontal or vertical direction. After rotating the input image clockwise by this angle, the recognized text lines become horizontal or vertical. In combination with the orientation property it can be used to overlay recognition results correctly on the original image, by rotating either the original image or recognition results by a suitable angle around the center of the original image. If the angle cannot be confidently detected, this property is not present. If the image contains text at different angles, only part of the text will be recognized correctly.", + "schema": { + "$ref": "#/definitions/OcrResult" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Ocr request": { + "$ref": "./examples/SuccessfulOcrWithUrl.json" + } + } + } + }, + "/describe": { + "post": { + "description": "This operation generates a description of an image in human readable language with complete sentences. The description is based on a collection of content tags, which are also returned by the operation. More than one description can be generated for each image. Descriptions are ordered by their confidence score. All descriptions are in English. Two input methods are supported -- (1) Uploading an image or (2) specifying an image URL.A successful response will be returned in JSON. If the request failed, the response will contain an error code and a message to help understand what went wrong.", + "operationId": "DescribeImage", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "maxCandidates", + "in": "query", + "description": "Maximum number of candidate descriptions to be returned. The default is 1.", + "type": "string", + "required": false, + "default": "1" + }, + { + "$ref": "#/parameters/ServiceLanguage" + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageUrl" + } + ], + "responses": { + "200": { + "description": "Image description object.", + "schema": { + "$ref": "#/definitions/ImageDescription" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Describe request": { + "$ref": "./examples/SuccessfulDescribeWithUrl.json" + } + } + } + }, + "/tag": { + "post": { + "description": "This operation generates a list of words, or tags, that are relevant to the content of the supplied image. The Computer Vision API can return tags based on objects, living beings, scenery or actions found in images. Unlike categories, tags are not organized according to a hierarchical classification system, but correspond to image content. Tags may contain hints to avoid ambiguity or provide context, for example the tag 'cello' may be accompanied by the hint 'musical instrument'. All tags are in English.", + "operationId": "TagImage", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "$ref": "#/parameters/ServiceLanguage" + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageUrl" + } + ], + "responses": { + "200": { + "description": "Image tags object.", + "schema": { + "$ref": "#/definitions/TagResult" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Tag request": { + "$ref": "./examples/SuccessfulTagWithUrl.json" + } + } + } + }, + "/models/{model}/analyze": { + "post": { + "description": "This operation recognizes content within an image by applying a domain-specific model. The list of domain-specific models that are supported by the Computer Vision API can be retrieved using the /models GET request. Currently, the API only provides a single domain-specific model: celebrities. Two input methods are supported -- (1) Uploading an image or (2) specifying an image URL. A successful response will be returned in JSON. If the request failed, the response will contain an error code and a message to help understand what went wrong.", + "operationId": "AnalyzeImageByDomain", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "model", + "in": "path", + "description": "The domain-specific content to recognize.", + "required": true, + "type": "string" + }, + { + "$ref": "#/parameters/ServiceLanguage" + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageUrl" + } + ], + "responses": { + "200": { + "description": "Analysis result based on the domain model", + "schema": { + "$ref": "#/definitions/DomainModelResults" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Domain Model analysis request": { + "$ref": "./examples/SuccessfulDomainModelWithUrl.json" + } + } + } + }, + "/recognizeText": { + "post": { + "description": "Recognize Text operation. When you use the Recognize Text interface, the response contains a field called 'Operation-Location'. The 'Operation-Location' field contains the URL that you must use for your Get Handwritten Text Operation Result operation.", + "operationId": "RecognizeText", + "parameters": [ + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageUrl" + }, + { + "$ref": "#/parameters/HandwritingBoolean" + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "The service has accepted the request and will start processing later. It will return Accepted immediately and include an Operation-Location header. Client side should further query the operation status using the URL specified in this header. The operation ID will expire in 48 hours.", + "headers": { + "Operation-Location": { + "description": "URL to query for status of the operation. The operation ID will expire in 48 hours. ", + "type": "string" + } + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Domain Model analysis request": { + "$ref": "./examples/SuccessfulRecognizeTextWithUrl.json" + } + } + } + }, + "/textOperations/{operationId}": { + "get": { + "description": "This interface is used for getting text operation result. The URL to this interface should be retrieved from 'Operation-Location' field returned from Recognize Text interface.", + "operationId": "GetTextOperationResult", + "parameters": [ + { + "name": "operationId", + "in": "path", + "description": "Id of the text operation returned in the response of the 'Recognize Handwritten Text'", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Returns the operation status.", + "schema": { + "$ref": "#/definitions/TextOperationResult" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Domain Model analysis request": { + "$ref": "./examples/SuccessfulGetTextOperationResult.json" + } + } + } + } + }, + "x-ms-paths": { + "/analyze?overload=stream": { + "post": { + "description": "This operation extracts a rich set of visual features based on the image content.", + "operationId": "AnalyzeImageInStream", + "consumes": [ + "application/octet-stream", + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "$ref": "#/parameters/VisualFeatures" + }, + { + "name": "details", + "in": "query", + "description": "A string indicating which domain-specific details to return. Multiple values should be comma-separated. Valid visual feature types include:Celebrities - identifies celebrities if detected in the image.", + "type": "string", + "required": false, + "enum": [ + "Celebrities", + "Landmarks" + ] + }, + { + "$ref": "#/parameters/ServiceLanguage" + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageStream" + } + ], + "responses": { + "200": { + "description": "The response include the extracted features in JSON format. Here is the definitions for enumeration types clipart = 0, ambiguous = 1, normal-clipart = 2, good-clipart = 3. Non-LineDrawing = 0,LineDrawing = 1.", + "schema": { + "$ref": "#/definitions/ImageAnalysis" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Analyze with Url request": { + "$ref": "./examples/SuccessfulAnalyzeWithStream.json" + } + } + } + }, + "/generateThumbnail?overload=stream": { + "post": { + "description": "This operation generates a thumbnail image with the user-specified width and height. By default, the service analyzes the image, identifies the region of interest (ROI), and generates smart cropping coordinates based on the ROI. Smart cropping helps when you specify an aspect ratio that differs from that of the input image. A successful response contains the thumbnail image binary. If the request failed, the response contains an error code and a message to help determine what went wrong.", + "operationId": "GenerateThumbnailInStream", + "consumes": [ + "application/octet-stream", + "multipart/form-data" + ], + "produces": [ + "application/octet-stream" + ], + "parameters": [ + { + "name": "width", + "type": "integer", + "in": "query", + "required": true, + "minimum": 1, + "maximum": 1023, + "description": "Width of the thumbnail. It must be between 1 and 1024. Recommended minimum of 50." + }, + { + "name": "height", + "type": "integer", + "in": "query", + "required": true, + "minimum": 1, + "maximum": 1023, + "description": "Height of the thumbnail. It must be between 1 and 1024. Recommended minimum of 50." + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageStream" + }, + { + "name": "smartCropping", + "type": "boolean", + "in": "query", + "required": false, + "default": false, + "description": "Boolean flag for enabling smart cropping." + } + ], + "responses": { + "200": { + "description": "The generated thumbnail in binary format.", + "schema": { + "type": "file" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Generate Thumbnail request": { + "$ref": "./examples/SuccessfulGenerateThumbnailWithStream.json" + } + } + } + }, + "/ocr?overload=stream": { + "post": { + "description": "Optical Character Recognition (OCR) detects printed text in an image and extracts the recognized characters into a machine-usable character stream. Upon success, the OCR results will be returned. Upon failure, the error code together with an error message will be returned. The error code can be one of InvalidImageUrl, InvalidImageFormat, InvalidImageSize, NotSupportedImage, NotSupportedLanguage, or InternalServerError.", + "operationId": "RecognizePrintedTextInStream", + "consumes": [ + "application/octet-stream", + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "$ref": "#/parameters/OcrLanguage" + }, + { + "$ref": "#/parameters/DetectOrientation" + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageStream" + } + ], + "responses": { + "200": { + "description": "The OCR results in the hierarchy of region/line/word. The results include text, bounding box for regions, lines and words. The angle, in degrees, of the detected text with respect to the closest horizontal or vertical direction. After rotating the input image clockwise by this angle, the recognized text lines become horizontal or vertical. In combination with the orientation property it can be used to overlay recognition results correctly on the original image, by rotating either the original image or recognition results by a suitable angle around the center of the original image. If the angle cannot be confidently detected, this property is not present. If the image contains text at different angles, only part of the text will be recognized correctly.", + "schema": { + "$ref": "#/definitions/OcrResult" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Ocr request": { + "$ref": "./examples/SuccessfulOcrWithStream.json" + } + } + } + }, + "/describe?overload=stream": { + "post": { + "description": "This operation generates a description of an image in human readable language with complete sentences. The description is based on a collection of content tags, which are also returned by the operation. More than one description can be generated for each image. Descriptions are ordered by their confidence score. All descriptions are in English. Two input methods are supported -- (1) Uploading an image or (2) specifying an image URL.A successful response will be returned in JSON. If the request failed, the response will contain an error code and a message to help understand what went wrong.", + "operationId": "DescribeImageInStream", + "consumes": [ + "application/octet-stream", + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "maxCandidates", + "in": "query", + "description": "Maximum number of candidate descriptions to be returned. The default is 1.", + "type": "string", + "required": false, + "default": "1" + }, + { + "$ref": "#/parameters/ServiceLanguage" + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageStream" + } + ], + "responses": { + "200": { + "description": "Image description object.", + "schema": { + "$ref": "#/definitions/ImageDescription" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Describe request": { + "$ref": "./examples/SuccessfulDescribeWithStream.json" + } + } + } + }, + "/tag?overload=stream": { + "post": { + "description": "This operation generates a list of words, or tags, that are relevant to the content of the supplied image. The Computer Vision API can return tags based on objects, living beings, scenery or actions found in images. Unlike categories, tags are not organized according to a hierarchical classification system, but correspond to image content. Tags may contain hints to avoid ambiguity or provide context, for example the tag 'cello' may be accompanied by the hint 'musical instrument'. All tags are in English.", + "operationId": "TagImageInStream", + "consumes": [ + "application/octet-stream", + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "$ref": "#/parameters/ServiceLanguage" + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageStream" + } + ], + "responses": { + "200": { + "description": "Image tags object.", + "schema": { + "$ref": "#/definitions/TagResult" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Tag request": { + "$ref": "./examples/SuccessfulTagWithStream.json" + } + } + } + }, + "/models/{model}/analyze?overload=stream": { + "post": { + "description": "This operation recognizes content within an image by applying a domain-specific model. The list of domain-specific models that are supported by the Computer Vision API can be retrieved using the /models GET request. Currently, the API only provides a single domain-specific model: celebrities. Two input methods are supported -- (1) Uploading an image or (2) specifying an image URL. A successful response will be returned in JSON. If the request failed, the response will contain an error code and a message to help understand what went wrong.", + "operationId": "AnalyzeImageByDomainInStream", + "consumes": [ + "application/octet-stream", + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "model", + "in": "path", + "description": "The domain-specific content to recognize.", + "required": true, + "type": "string" + }, + { + "$ref": "#/parameters/ServiceLanguage" + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageStream" + } + ], + "responses": { + "200": { + "description": "Analysis result based on the domain model", + "schema": { + "$ref": "#/definitions/DomainModelResults" + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Domain Model analysis request": { + "$ref": "./examples/SuccessfulDomainModelWithStream.json" + } + } + } + }, + "/recognizeText?overload=stream": { + "post": { + "description": "Recognize Text operation. When you use the Recognize Text interface, the response contains a field called 'Operation-Location'. The 'Operation-Location' field contains the URL that you must use for your Get Handwritten Text Operation Result operation.", + "operationId": "RecognizeTextInStream", + "parameters": [ + { + "$ref": "#/parameters/HandwritingBoolean" + }, + { + "$ref": "../../../Common/Parameters.json#/parameters/ImageStream" + } + ], + "consumes": [ + "application/octet-stream" + ], + "produces": [ + "application/json" + ], + "responses": { + "202": { + "description": "The service has accepted the request and will start processing later.", + "headers": { + "Operation-Location": { + "description": "URL to query for status of the operation. The operation ID will expire in 48 hours. ", + "type": "string" + } + } + }, + "default": { + "description": "Error response.", + "schema": { + "$ref": "#/definitions/ComputerVisionError" + } + } + }, + "x-ms-examples": { + "Successful Domain Model analysis request": { + "$ref": "./examples/SuccessfulRecognizeTextWithStream.json" + } + } + } + } + }, + "definitions": { + "TextOperationResult": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Status of the text operation.", + "enum": [ + "Not Started", + "Running", + "Failed", + "Succeeded" + ], + "x-ms-enum": { + "name": "TextOperationStatusCodes", + "modelAsString": false + }, + "x-nullable": false + }, + "recognitionResult": { + "$ref": "#/definitions/RecognitionResult" + } + } + }, + "RecognitionResult": { + "type": "object", + "properties": { + "lines": { + "type": "array", + "items": { + "$ref": "#/definitions/Line" + } + } + } + }, + "Line": { + "type": "object", + "properties": { + "boundingBox": { + "$ref": "#/definitions/BoundingBox" + }, + "text": { + "type": "string" + }, + "words": { + "type": "array", + "items": { + "$ref": "#/definitions/Word" + } + } + } + }, + "Word": { + "type": "object", + "properties": { + "boundingBox": { + "$ref": "#/definitions/BoundingBox" + }, + "text": { + "type": "string" + } + } + }, + "BoundingBox": { + "type": "array", + "items": { + "type": "integer", + "x-nullable": false + } + }, + "ImageAnalysis": { + "type": "object", + "description": "Result of AnalyzeImage operation.", + "properties": { + "categories": { + "type": "array", + "description": "An array indicating identified categories.", + "items": { + "$ref": "#/definitions/Category" + } + }, + "adult": { + "$ref": "#/definitions/AdultInfo" + }, + "color": { + "$ref": "#/definitions/ColorInfo" + }, + "imageType": { + "$ref": "#/definitions/ImageType" + }, + "tags": { + "type": "array", + "description": "A list of tags with confidence level.", + "items": { + "$ref": "#/definitions/ImageTag" + } + }, + "description": { + "$ref": "#/definitions/ImageDescriptionDetails" + }, + "faces": { + "type": "array", + "description": "An array of possible faces within the image.", + "items": { + "$ref": "#/definitions/FaceDescription" + } + }, + "requestId": { + "type": "string", + "description": "Id of the request for tracking purposes." + }, + "metadata": { + "$ref": "#/definitions/ImageMetadata" + } + } + }, + "OcrResult": { + "type": "object", + "properties": { + "language": { + "type": "string", + "description": "The BCP-47 language code of the text in the image." + }, + "textAngle": { + "type": "number", + "format": "double", + "description": "The angle, in degrees, of the detected text with respect to the closest horizontal or vertical direction. After rotating the input image clockwise by this angle, the recognized text lines become horizontal or vertical. In combination with the orientation property it can be used to overlay recognition results correctly on the original image, by rotating either the original image or recognition results by a suitable angle around the center of the original image. If the angle cannot be confidently detected, this property is not present. If the image contains text at different angles, only part of the text will be recognized correctly." + }, + "orientation": { + "type": "string", + "description": "Orientation of the text recognized in the image. The value (up,down,left, or right) refers to the direction that the top of the recognized text is facing, after the image has been rotated around its center according to the detected text angle (see textAngle property)." + }, + "regions": { + "type": "array", + "description": "An array of objects, where each object represents a region of recognized text.", + "items": { + "$ref": "#/definitions/OcrRegion" + } + } + } + }, + "OcrRegion": { + "type": "object", + "description": "A region consists of multiple lines (e.g. a column of text in a multi-column document).", + "properties": { + "boundingBox": { + "type": "string", + "description": "Bounding box of a recognized region. The four integers represent the x-coordinate of the left edge, the y-coordinate of the top edge, width, and height of the bounding box, in the coordinate system of the input image, after it has been rotated around its center according to the detected text angle (see textAngle property), with the origin at the top-left corner, and the y-axis pointing down." + }, + "lines": { + "type": "array", + "items": { + "$ref": "#/definitions/OcrLine" + } + } + } + }, + "OcrLine": { + "type": "object", + "description": "An object describing a single recognized line of text.", + "properties": { + "boundingBox": { + "type": "string", + "description": "Bounding box of a recognized line. The four integers represent the x-coordinate of the left edge, the y-coordinate of the top edge, width, and height of the bounding box, in the coordinate system of the input image, after it has been rotated around its center according to the detected text angle (see textAngle property), with the origin at the top-left corner, and the y-axis pointing down." + }, + "words": { + "type": "array", + "description": "An array of objects, where each object represents a recognized word.", + "items": { + "$ref": "#/definitions/OcrWord" + } + } + } + }, + "OcrWord": { + "type": "object", + "description": "Information on a recognized word.", + "properties": { + "boundingBox": { + "type": "string", + "description": "Bounding box of a recognized word. The four integers represent the x-coordinate of the left edge, the y-coordinate of the top edge, width, and height of the bounding box, in the coordinate system of the input image, after it has been rotated around its center according to the detected text angle (see textAngle property), with the origin at the top-left corner, and the y-axis pointing down." + }, + "text": { + "type": "string", + "description": "String value of a recognized word." + } + } + }, + "ListModelsResult": { + "type": "object", + "description": "Result of the List Domain Models operation.", + "properties": { + "models": { + "type": "array", + "readOnly": true, + "description": "An array of supported models.", + "items": { + "$ref": "#/definitions/ModelDescription" + } + } + } + }, + "ModelDescription": { + "type": "object", + "description": "An object describing supported model by name and categories.", + "properties": { + "name": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "DomainModelResults": { + "type": "object", + "description": "Result of image analysis using a specific domain model including additional metadata.", + "properties": { + "result": { + "x-ms-client-flatten": true, + "type": "object", + "description": "Model-specific response" + }, + "requestId": { + "type": "string", + "description": "Id of the REST API request." + }, + "metadata": { + "$ref": "#/definitions/ImageMetadata" + } + } + }, + "CelebrityResults": { + "type": "object", + "description": "List of celebrities recognized in the image.", + "properties": { + "celebrities": { + "type": "array", + "items": { + "$ref": "#/definitions/CelebritiesModel" + } + }, + "requestId": { + "type": "string", + "description": "Id of the REST API request." + }, + "metadata": { + "$ref": "#/definitions/ImageMetadata" + } + } + }, + "LandmarkResults": { + "type": "object", + "description": "List of landmarks recognized in the image.", + "properties": { + "landmarks": { + "type": "array", + "items": { + "type": "object", + "description": "A landmark recognized in the image", + "properties": { + "name": { + "type": "string", + "description": "Name of the landmark." + }, + "confidence": { + "type": "number", + "format": "double", + "description": "Confidence level for the landmark recognition." + } + } + } + }, + "requestId": { + "type": "string", + "description": "Id of the REST API request." + }, + "metadata": { + "$ref": "#/definitions/ImageMetadata" + } + } + }, + "ImageDescription": { + "type": "object", + "description": "A collection of content tags, along with a list of captions sorted by confidence level, and image metadata.", + "properties": { + "description": { + "x-ms-client-flatten": true, + "$ref": "#/definitions/ImageDescriptionDetails" + } + } + }, + "TagResult": { + "type": "object", + "description": "The results of a image tag operation, including any tags and image metadata.", + "properties": { + "tags": { + "type": "array", + "description": "A list of tags with confidence level.", + "items": { + "$ref": "#/definitions/ImageTag" + } + }, + "requestId": { + "type": "string", + "description": "Id of the REST API request." + }, + "metadata": { + "$ref": "#/definitions/ImageMetadata" + } + } + }, + "ImageDescriptionDetails": { + "type": "object", + "description": "A collection of content tags, along with a list of captions sorted by confidence level, and image metadata.", + "properties": { + "tags": { + "type": "array", + "description": "A collection of image tags.", + "items": { + "type": "string" + } + }, + "captions": { + "type": "array", + "description": "A list of captions, sorted by confidence level.", + "items": { + "$ref": "#/definitions/ImageCaption" + } + }, + "requestId": { + "type": "string", + "description": "Id of the REST API request." + }, + "metadata": { + "$ref": "#/definitions/ImageMetadata" + } + } + }, + "ImageCaption": { + "type": "object", + "description": "An image caption, i.e. a brief description of what the image depicts.", + "properties": { + "text": { + "type": "string", + "description": "The text of the caption" + }, + "confidence": { + "type": "number", + "format": "double", + "description": "The level of confidence the service has in the caption" + } + } + }, + "ImageTag": { + "type": "object", + "description": "An image caption, i.e. a brief description of what the image depicts.", + "properties": { + "name": { + "type": "string", + "description": "The tag value" + }, + "confidence": { + "type": "number", + "format": "double", + "description": "The level of confidence the service has in the caption" + } + } + }, + "ImageMetadata": { + "type": "object", + "description": "Image metadata", + "properties": { + "width": { + "type": "integer", + "format": "int32", + "description": "Image width" + }, + "height": { + "type": "integer", + "format": "int32", + "description": "Image height" + }, + "format": { + "type": "string", + "description": "Image format" + } + } + }, + "CelebritiesModel": { + "type": "object", + "description": "An object describing possible celebrity identification.", + "properties": { + "name": { + "type": "string", + "description": "Name of the celebrity." + }, + "confidence": { + "type": "number", + "format": "double", + "description": "Level of confidence ranging from 0 to 1." + }, + "faceRectangle": { + "$ref": "#/definitions/FaceRectangle" + } + } + }, + "FaceRectangle": { + "type": "object", + "description": "An object describing face rectangle.", + "properties": { + "left": { + "type": "integer", + "description": "X-coordinate of the top left point of the face." + }, + "top": { + "type": "integer", + "description": "Y-coordinate of the top left point of the face." + }, + "width": { + "type": "integer", + "description": "Width measured from the top-left point of the face." + }, + "height": { + "type": "integer", + "description": "Height measured from the top-left point of the face." + } + } + }, + "FaceDescription": { + "type": "object", + "description": "An object describing a face identified in the image.", + "properties": { + "age": { + "type": "integer", + "description": "Possible age of the face." + }, + "gender": { + "type": "string", + "description": "Possible gender of the face.", + "x-ms-enum": { + "name": "Gender-", + "modelAsString": false + }, + "enum": [ + "Male", + "Female" + ] + }, + "faceRectangle": { + "$ref": "#/definitions/FaceRectangle" + } + } + }, + "ImageType": { + "type": "object", + "description": "An object providing possible image types and matching confidence levels.", + "properties": { + "clipArtType": { + "type": "number", + "description": "Confidence level that the image is a clip art." + }, + "lineDrawingType": { + "type": "number", + "description": "Confidence level that the image is a line drawing." + } + } + }, + "ColorInfo": { + "type": "object", + "description": "An object providing additional metadata describing color attributes.", + "properties": { + "dominantColorForeground": { + "type": "string", + "description": "Possible dominant foreground color." + }, + "dominantColorBackground": { + "type": "string", + "description": "Possible dominant background color." + }, + "dominantColors": { + "type": "array", + "description": "An array of possible dominant colors.", + "items": { + "type": "string" + } + }, + "accentColor": { + "type": "string", + "description": "Possible accent color." + }, + "isBWImg": { + "type": "boolean", + "description": "A value indicating if the image is black and white." + } + } + }, + "AdultInfo": { + "type": "object", + "description": "An object describing whether the image contains adult-oriented content and/or is racy.", + "properties": { + "isAdultContent": { + "type": "boolean", + "x-nullable": false, + "description": "A value indicating if the image contains adult-oriented content." + }, + "isRacyContent": { + "type": "boolean", + "x-nullable": false, + "description": "A value indicating if the image is race." + }, + "adultScore": { + "type": "number", + "format": "double", + "x-nullable": false, + "description": "Score from 0 to 1 that indicates how much of adult content is within the image." + }, + "racyScore": { + "type": "number", + "format": "double", + "x-nullable": false, + "description": "Score from 0 to 1 that indicates how suggestive is the image." + } + } + }, + "Category": { + "type": "object", + "description": "An object describing identified category.", + "properties": { + "name": { + "type": "string", + "description": "Name of the category." + }, + "score": { + "type": "number", + "format": "double", + "description": "Scoring of the category." + }, + "detail": { + "$ref": "#/definitions/CategoryDetail" + } + } + }, + "CategoryDetail": { + "type": "object", + "description": "An object describing additional category details.", + "properties": { + "celebrities": { + "type": "array", + "description": "An array of celebrities if any identified.", + "items": { + "$ref": "#/definitions/CelebritiesModel" + } + } + } + }, + "ComputerVisionError": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "The error code.", + "enum": [ + "InvalidImageUrl", + "InvalidImageFormat", + "InvalidImageSize", + "NotSupportedVisualFeature", + "NotSupportedImage", + "InvalidDetails", + "NotSupportedLanguage", + "BadArgument", + "FailedToProcess", + "Timeout", + "InternalServerError", + "Unspecified", + "StorageException" + ], + "x-ms-enum": { + "name": "ComputerVisionErrorCodes", + "modelAsString": false + } + }, + "message": { + "type": "string", + "description": "A message explaining the error reported by the service." + }, + "requestId": { + "type": "string", + "description": "A unique request identifier." + } + } + }, + "ServiceLanguage": { + "type": "string" + } + }, + "parameters": { + "VisualFeatures": { + "name": "visualFeatures", + "in": "query", + "description": "A string indicating what visual feature types to return. Multiple values should be comma-separated. Valid visual feature types include:Categories - categorizes image content according to a taxonomy defined in documentation. Tags - tags the image with a detailed list of words related to the image content. Description - describes the image content with a complete English sentence. Faces - detects if faces are present. If present, generate coordinates, gender and age. ImageType - detects if image is clipart or a line drawing. Color - determines the accent color, dominant color, and whether an image is black&white.Adult - detects if the image is pornographic in nature (depicts nudity or a sex act). Sexually suggestive content is also detected.", + "type": "array", + "x-ms-parameter-location": "method", + "required": false, + "collectionFormat": "csv", + "items": { + "type": "string", + "x-nullable": false, + "x-ms-enum": { + "name": "VisualFeatureTypes", + "modelAsString": false + }, + "enum": [ + "ImageType", + "Faces", + "Adult", + "Categories", + "Color", + "Tags", + "Description" + ] + } + }, + "OcrLanguage": { + "name": "language", + "in": "query", + "description": "The BCP-47 language code of the text to be detected in the image. The default value is 'unk'", + "type": "string", + "required": false, + "x-ms-parameter-location": "method", + "x-nullable": false, + "x-ms-enum": { + "name": "OcrLanguages", + "modelAsString": false + }, + "default": "unk", + "enum": [ + "unk", + "zh-Hans", + "zh-Hant", + "cs", + "da", + "nl", + "en", + "fi", + "fr", + "de", + "el", + "hu", + "it", + "ja", + "ko", + "nb", + "pl", + "pt", + "ru", + "es", + "sv", + "tr", + "ar", + "ro", + "sr-Cyrl", + "sr-Latn", + "sk" + ] + }, + "DetectOrientation": { + "name": "detectOrientation", + "in": "query", + "description": "Whether detect the text orientation in the image. With detectOrientation=true the OCR service tries to detect the image orientation and correct it before further processing (e.g. if it's upside-down). ", + "required": true, + "x-ms-parameter-location": "method", + "type": "boolean", + "default": true + }, + "HandwritingBoolean": { + "name": "detectHandwriting", + "in": "query", + "description": "If 'true' is specified, handwriting recognition is performed. If this parameter is set to 'false' or is not specified, printed text recognition is performed.", + "required": false, + "x-ms-parameter-location": "method", + "type": "boolean", + "default": false + }, + "ServiceLanguage": { + "name": "language", + "in": "query", + "description": "The desired language for output generation. If this parameter is not specified, the default value is "en".Supported languages:en - English, Default. es - Spanish, ja - Japanese, pt - Portuguese, zh - Simplified Chinese.", + "type": "string", + "required": false, + "x-ms-parameter-location": "method", + "x-nullable": false, + "default": "en", + "enum": [ + "en", + "es", + "ja", + "pt", + "zh" + ] + } + } +} diff --git a/modules/swagger-parser/src/test/resources/parameters-external/readme.txt b/modules/swagger-parser/src/test/resources/parameters-external/readme.txt new file mode 100644 index 0000000000..4e5df4c44d --- /dev/null +++ b/modules/swagger-parser/src/test/resources/parameters-external/readme.txt @@ -0,0 +1 @@ +The files in 'data-plane' are from Microsoft Azure's GitHub repository https://github.com/Azure/azure-rest-api-specs MIT licensed. diff --git a/modules/swagger-parser/src/test/resources/parameters-external/simple/externals-level-0.json b/modules/swagger-parser/src/test/resources/parameters-external/simple/externals-level-0.json new file mode 100644 index 0000000000..47c51df5e9 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/parameters-external/simple/externals-level-0.json @@ -0,0 +1,51 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "Some Store" + }, + "paths": { + "/path-1": { + "get": { + "parameters": [ + { + "$ref": "externals-level-1.json#/parameters/P-Level1Thing1" + } + ], + "responses": { + "200": { + "description": "Returns all the stuff." + } + } + } + }, + "/path-2": { + "get": { + "parameters": [ + { + "$ref": "externals-level-1.json#/parameters/P-Level1Thing2" + } + ], + "responses": { + "200": { + "description": "Returns all the stuff." + } + } + } + }, + "/path-3": { + "get": { + "parameters": [ + { + "$ref": "externals-level-1.json#/parameters/P-Level1Thing3" + } + ], + "responses": { + "200": { + "description": "Returns all the stuff." + } + } + } + } + } +} \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/parameters-external/simple/externals-level-1.json b/modules/swagger-parser/src/test/resources/parameters-external/simple/externals-level-1.json new file mode 100644 index 0000000000..f3af362160 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/parameters-external/simple/externals-level-1.json @@ -0,0 +1,42 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "Pets Store" + }, + "parameters": { + "P-Level1Thing1": { + "in": "body", + "name": "Level1Thing1", + "schema": { + "type": "string" + } + }, + "P-Level1Thing2": { + "name": "Level1Thing2", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/D-Level1Thing3" + } + }, + "P-Level1Thing3": { + "name": "Level1Thing3", + "in": "body", + "required": true, + "schema": { + "$ref": "externals-level-2.json#/parameters/P-Level2Thing3" + } + } + }, + "definitions": { + "D-Level1Thing3": { + "name": "Level1Thing3", + "type": "string" + }, + "D-Level1Thing4": { + "name": "Level1Thing4", + "type": "string" + } + } +} diff --git a/modules/swagger-parser/src/test/resources/parameters-external/simple/externals-level-2.json b/modules/swagger-parser/src/test/resources/parameters-external/simple/externals-level-2.json new file mode 100644 index 0000000000..9cf416d51e --- /dev/null +++ b/modules/swagger-parser/src/test/resources/parameters-external/simple/externals-level-2.json @@ -0,0 +1,42 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "Pets Store" + }, + "parameters": { + "P-Level2Thing1": { + "in": "body", + "name": "Level2Thing1", + "schema": { + "type": "string" + } + }, + "P-Level2Thing2": { + "name": "Level2Thing2", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/D-Level2Thing3" + } + }, + "P-Level2Thing3": { + "name": "Level2Thing3", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/D-Level2Thing4" + } + } + }, + "definitions": { + "D-Level2Thing3": { + "name": "Level2Thing3", + "type": "string" + }, + "D-Level2Thing4": { + "name": "Level2Thing4", + "type": "string" + } + } +} \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/petstore.json b/modules/swagger-parser/src/test/resources/petstore.json index d847efd256..b28925899b 100644 --- a/modules/swagger-parser/src/test/resources/petstore.json +++ b/modules/swagger-parser/src/test/resources/petstore.json @@ -186,7 +186,7 @@ "type": "string" }, "collectionFormat": "pipes", - "default": "available" + "default": ["available"] } ], "responses": { diff --git a/modules/swagger-parser/src/test/resources/relative-file-references/json/parent.json b/modules/swagger-parser/src/test/resources/relative-file-references/json/parent.json index d833a49bd2..0a3bdb1e43 100644 --- a/modules/swagger-parser/src/test/resources/relative-file-references/json/parent.json +++ b/modules/swagger-parser/src/test/resources/relative-file-references/json/parent.json @@ -5,6 +5,11 @@ "$ref": "./paths/healthPath.json" } }, + "parameters": { + "param2": { + "$ref": "./parameters/params.json#/param2" + } + }, "definitions": { "refInDefinitions" : { "$ref": "./models/example.json" diff --git a/modules/swagger-parser/src/test/resources/relative-file-references/yaml/parent.yaml b/modules/swagger-parser/src/test/resources/relative-file-references/yaml/parent.yaml index f25083b2f2..b32489749e 100644 --- a/modules/swagger-parser/src/test/resources/relative-file-references/yaml/parent.yaml +++ b/modules/swagger-parser/src/test/resources/relative-file-references/yaml/parent.yaml @@ -3,6 +3,9 @@ swagger: "2.0" paths: /health: $ref: "./paths/healthPath.yaml" +parameters: + param2: + $ref: "./parameters/params.yaml#/param2" definitions: refInDefinitions: $ref: "./models/example.yaml" diff --git a/modules/swagger-parser/src/test/resources/relative-file-references/yaml/paths/healthPath.yaml b/modules/swagger-parser/src/test/resources/relative-file-references/yaml/paths/healthPath.yaml index 2840ec8fd7..6aa4b603f6 100644 --- a/modules/swagger-parser/src/test/resources/relative-file-references/yaml/paths/healthPath.yaml +++ b/modules/swagger-parser/src/test/resources/relative-file-references/yaml/paths/healthPath.yaml @@ -16,11 +16,11 @@ get: schema: $ref: "../models/health.yaml" responses: - 200: + "200": description: "Health information from the server" schema: $ref: "../models/health.yaml" - 400: + "400": $ref: "../responses/errorResponses.yaml#/bad-request" - 500: + "500": $ref: "../responses/errorResponses.yaml#/internal-server-error" diff --git a/modules/swagger-parser/src/test/resources/relativeTest.yaml b/modules/swagger-parser/src/test/resources/relativeTest.yaml new file mode 100644 index 0000000000..58babfa390 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/relativeTest.yaml @@ -0,0 +1,10 @@ +RelativeObj: + type: object + properties: + lorem: + type: object + properties: + firstName: + type: string + lastName: + type: string \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/safelyResolve/oas20SafeUrlResolvingWithLocalhost.yaml b/modules/swagger-parser/src/test/resources/safelyResolve/oas20SafeUrlResolvingWithLocalhost.yaml new file mode 100644 index 0000000000..60ffcef8ce --- /dev/null +++ b/modules/swagger-parser/src/test/resources/safelyResolve/oas20SafeUrlResolvingWithLocalhost.yaml @@ -0,0 +1,26 @@ +swagger: '2.0' +info: + version: "1.0.0" + title: ssrf-test + +consumes: + - application/json +produces: + - application/json +paths: + /devices: + get: + operationId: getDevices + responses: + '200': + description: All the devices + schema: + $ref: 'http://localhost/example' + /pets: + get: + operationId: getPets + responses: + '200': + description: All the pets + schema: + $ref: 'https://petstore.swagger.io/v2/swagger.json' \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/safelyResolve/oas20SafeUrlResolvingWithPetstore.yaml b/modules/swagger-parser/src/test/resources/safelyResolve/oas20SafeUrlResolvingWithPetstore.yaml new file mode 100644 index 0000000000..2982fd96a6 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/safelyResolve/oas20SafeUrlResolvingWithPetstore.yaml @@ -0,0 +1,26 @@ +swagger: '2.0' +info: + version: "1.0.0" + title: ssrf-test + +consumes: + - application/json +produces: + - application/json +paths: + /devices: + get: + operationId: getDevices + responses: + '200': + description: All the devices + schema: + $ref: 'https://petstore3.swagger.io/api/v3/openapi.json' + /pets: + get: + operationId: getPets + responses: + '200': + description: All the pets + schema: + $ref: 'https://petstore.swagger.io/v2/swagger.json' \ No newline at end of file diff --git a/modules/swagger-parser/src/test/resources/swagger-reference-response.yaml b/modules/swagger-parser/src/test/resources/swagger-reference-response.yaml new file mode 100644 index 0000000000..9008b798ec --- /dev/null +++ b/modules/swagger-parser/src/test/resources/swagger-reference-response.yaml @@ -0,0 +1,41 @@ +swagger: '2.0' +info: + version: "2.0" + title: Marketplaces - Orders - Subscriptions + description: The order subscription management + + contact: + email: help@test.com + + license: + name: Test + url: http://www.test.com + + +security: + - api_key: [] + +consumes: + - application/json +produces: + - application/json + + +paths: + + '/': + get: + tags: + - Subscriptions + operationId: GetSubscriptionList + summary: Get the subscription list + responses: + 200: + $ref: 'folder/domains/Test/api.test.com/v2.yaml#/responses/GeneralError' + deprecated: false + + +definitions: {} + +host: api.test.com +basePath: /v2/user/marketplaces/orders \ No newline at end of file diff --git a/pom.xml b/pom.xml index cddc97e54e..3af31686e6 100644 --- a/pom.xml +++ b/pom.xml @@ -1,17 +1,14 @@ - - org.sonatype.oss - oss-parent - 5 - 4.0.0 io.swagger swagger-parser-project - 1.0.40-SNAPSHOT + 1.0.76-SNAPSHOT pom swagger-parser-project + swagger-parser-project + https://github.com/swagger-api/swagger-parser fehguy @@ -46,6 +43,11 @@ repo + + scm:git:git@github.com:swagger-api/swagger-parser.git + scm:git:git@github.com:swagger-api/swagger-parser.git + https://github.com/swagger-api/swagger-parser + install @@ -95,7 +97,7 @@ org.apache.maven.plugins maven-jar-plugin - 2.4 + 3.3.0 **/logback.xml @@ -112,19 +114,19 @@ maven-compiler-plugin - 3.5 + 3.13.0 - 1.7 - 1.7 + 1.8 + 1.8 org.apache.maven.plugins maven-javadoc-plugin - 2.7 + 3.6.2 true - 1.7 + 1.8 UTF-8 1g @@ -144,7 +146,7 @@ org.apache.maven.plugins maven-source-plugin - 2.1.2 + 3.3.0 attach-sources @@ -158,7 +160,7 @@ org.jacoco jacoco-maven-plugin - 0.7.5.201505241946 + 0.8.8 default-prepare-agent @@ -188,8 +190,115 @@ + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + + + enforce-no-snapshots + + enforce + + + + + No Snapshots Allowed! + true + + + true + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + true + + central + true + published + 3600 + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + + --pinentry-mode + loopback + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + + + + + + security + + + + org.owasp + dependency-check-maven + 6.5.3 + + true + + + + + check + + + + + + + + + release + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + + --pinentry-mode + loopback + + + + + sign-artifacts + verify + + sign + + + + + + + + @@ -202,6 +311,11 @@ swagger-models ${swagger-core-version} + + org.yaml + snakeyaml + ${snakeyaml-version} + org.testng testng @@ -235,17 +349,21 @@ + modules/swagger-parser-safe-url-resolver modules/swagger-parser modules/swagger-compat-spec-parser - 2.4 - 1.6.3 - 1.5.21 - 4.8.1 + + 2.15.1 + 2.4 + 2.0.9 + 1.6.16 + 4.13.2 6.9.6 1.19 - 2.4.1 - 2.18.1 + 2.27.2 + 2.22.2 + UTF-8