diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 00000000..cc7ce395
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,41 @@
+name: Publish
+
+on:
+ push:
+ tags:
+ - "*"
+
+jobs:
+ publish:
+ name: Publish
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: Set up JDK
+ uses: actions/setup-java@v3
+ with:
+ java-version: 8
+ distribution: 'temurin'
+ - name: Cache Gradle packages
+ uses: actions/cache@v3
+ with:
+ path: ~/.gradle/caches
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
+ restore-keys: ${{ runner.os }}-gradle
+ - name: Test
+ run: ./ci_test.sh
+ - name: Publish to Gradle Plugin Portal
+ env:
+ GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
+ GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
+ run: ./ci_publish_gradle.sh
+ - name: Publish to Maven Central
+ env:
+ FILE_ENCRYPTION_PASSWORD: ${{ secrets.FILE_ENCRYPTION_PASSWORD }}
+ SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
+ SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
+ SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
+ SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
+ run: ./ci_publish_java.sh -s
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..4791944b
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,39 @@
+name: Test
+on:
+ push:
+ branches:
+ - master
+ - 'maintenance/**'
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+jobs:
+ build:
+ name: Test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: Set up JDK
+ uses: actions/setup-java@v3
+ with:
+ java-version: 8
+ distribution: 'temurin'
+ - name: Cache SonarCloud packages
+ uses: actions/cache@v3
+ with:
+ path: ~/.sonar/cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+ - name: Cache Gradle packages
+ uses: actions/cache@v3
+ with:
+ path: ~/.gradle/caches
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
+ restore-keys: ${{ runner.os }}-gradle
+ - name: Test
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: ./ci_test.sh
diff --git a/.gitignore b/.gitignore
index 5d467697..102fc0b6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,12 @@
*.gpg
-
.gradle/
.*
!.gitignore
-!.travis.yml
-!.circleci
+!.github/
.settings/
build/
out/
bin/
-gradle.properties
*.iml
*.ipr
*.iws
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 6048d603..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-dist: focal
-before_install:
- - sudo apt-get install -y openjdk-8-jdk
-before_cache:
-- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
-- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
-cache:
- directories:
- - "$HOME/.gradle/caches/"
- - "$HOME/.gradle/wrapper/"
-install: true
-script: "./ci_build.sh"
-deploy:
-- provider: script
- script: "./ci_publish_java.sh"
- on:
- tags: true
- skip_cleanup: true
-- provider: script
- script: "./ci_publish_gradle.sh"
- on:
- tags: true
- skip_cleanup: true
-env:
- global:
- - CI_NAME=travis-ci
- - SIGNING_KEYRING_FILE="${TRAVIS_BUILD_DIR}/secret-keys.gpg"
- - JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
- # coveralls
- - secure: "nOkY2AcR5Z7hKKzi+zCzmzPk+xiRRrJjGliDJnHnEO7g+iJ8SmT+thHuMFcXqvjxJ006LbR3GPB8M8qgFXFDPEUDRPppShi3waOm1ddlM2iXh9kbCoROoioyRO+F07deH6ExibkLbu/xq3WwAAw7lx/ZP+buc2R2VCbV0nLKP9iE4nHQX8msLgmU1LjWf0Au1Pgl2eWXV0J4/ZJOvcVu9hvBf1Ow4C7BNb0KZmMjeT9uMK0hiGpb9VVbwedOWfsbaFmqmKYqVKS8UtNmG+HBjvZfajrIARKRPc9w9uEvfl4E1H/J7PJLy2kOrCOauj8LMu6DduSrWcg08T5VnH51cavSAsWWG1ImTgEsUBNpllc9r1XLhQCLQ1TWCgGmleqYjZ2ySvg0MQpFjTgXMZC+aDkjZjcEeq3BgrncAR/bcG4ByrZBWyoEXDxBwoMZnkryTxK07UUgXRXdjJUldJ5CQW9oSfd+oEXKJyqQGNt0ob3S0sRS5uhrKniK+6Mwzxf6vXeYvn2fVP5j3hocUz4XzDJQoDDmJi6D4BpwksqJ81CNOHpyeDIOhdXxvA/J8DMCK0Q7tTSbnSOxItaht56CSxqqLSGax3/4Nr+5QX9jpmdJ03HdpP+MOuIl6dZCmE4/w1IFaFEfhpETPlnWZHl1BsOt7omDeW7yCttDq3Y1mXU="
- # gradle plugin portal
- - secure: "XZ4MKhQkOcOp7iRpq/DPJu4RkGs6+26ykgo1n+NZDJlamJ+hikW0JahBuJdi30z/JVnXcpbHkVfmUSrHyWg0irggtOB+E566pRw/iPzeH6ZRzs0gFYdWujPv7BTXDZzzfGck8gSnMFDStHK1MYOYNYc8T1hNytwPy4WygGwc8mcgdbda5TTJuI0gbNm+5gehjW+h5tjeR6UrUBSj2Gbx+/UdJ3oXPOHFFu0i4OuXTSkWZz7Dz1M7o9loJOUtHwKB5osm9+tUQqLiI9yRE4Z80ngOC8eoF7HqBKFydoezqE+DCxNg+Vy+RkkTpa+mvBD+k5bZ/7BeFmOQdzE61YM1/xN1CaAjjFEI0KEisZ3gycdnxUp7sLkBuCMPfuhix3Sqjt9ulEv4k1MRMsXBJ7Wpe+ZuovHuXFlu1ni6tU5pRs5BhCnXc+X7dDvaprJUbB1pSp6/Rjpf5nOIszVf/HkO6MmgMQkZLwlcu6I8QPnOCfkW8FJroyOTUr/MD/oPNVGRLvANZuyqqNtEc9g/Nt6f0qSvlkdEOVcgBlz95d8UbkhEYbYbB71gqOo+flhfgIDOWEXgkrGjY7p95g5R+VIKSBaXv36EYyNPVAQQ55riyPnDpU5JUnur6GZIY+jTuNS0Tp6tXhPksRcBNjKEUqfte8YqI8EvdZfzvloODsb+9Wk="
- # gradle plugin portal
- - secure: "nd2+OblPYu7qdG0D/lkbKMhoxaEm/U/IWQQ7HJYK5Yjt6f/JOgux0OIA/m+OO/3wtWDTbT+ihRekIcIU/x3ab4b1P1cwP1pMDsqc8O3Nqa8K79AGOYx5c3vZ7M5DBiLYKZDSM7VAaz0ol1uoY7Lo0jCMJQsf74U4o2xunrok5bf511cAJZ4gcczZ3hfoTu98gonil71Ln9TrdE9f86dX/aI7hmD9dajjxz98JkQSR/Cf47W4oAbBla85rHlDdzzrnGuVyhpV3r1ib2lrpcrBHID5IhSXyCEJxdbCNAlTCGuXdBMG+m99h9hYw7Nq1OsxbO2gmqs77fXmwKpPGwtf5PLGlQdkcExsdvNORmTeIn5Ac8JNDiEz8w/jrpBqkVZ5x6yVeSUe5VqAR1GLCrqIud2W3dDefWsSEhHtvu/iJYPB8IN01S55V7Uzy7hfs4Z6DnjPxT5khRleUViQywhFZ4/+yQ5OELeLbrGaDwZuZI2WOMahvgV749NrdCZ6SeIZSptr6ywCH7OFUJIbgiYvv0wxgpqg3cYeQ5OsUFR/+dWkXA+rk+17N7PodCAHl9Cg1ckg1N5H2AYAQzxn8mSELb2STRZpViygbiDeadrjnJBr8kFYdWkFc4fTX2CeboD7MitUjOWXvvQrlyU+R9BVaxs1cV8iTCLnP1dt0HKXKBA="
- # SONATYPE_USERNAME
- - secure: "IVEnzxQmCuquvSjHDI5Y82j6WKo+TRGoUFZBY1n/oX7H+aAcOAizVQjXS/usJp/lo7BR7Hpov6DmvfPyKNHh4uWR8x5gx32ibo8Wah385jdSADZ4uZS2mDwt+bw5pPnzANBL7CnXz5JQdHrws3Iz+47p4sWbDmwaLX+hi8CIza+jMiLO57cpMNcuMsf9Pv5C/y5Vp+RvjE16b4JlXcdfgkK1uQG2jP3jPi0AVgIj7jubiJNgbN6YzxwUe4lDFq42/hmnm2FIvuUaUPn5lkKvs5xBdJp7FMBlii+LUMB9aP7dQbXdKu/BJZ7yxY2hbdNfydPbRdwXICJzUM/mi37GWZD4TbXDHDd0Hh2HasLU23R8dy9rRmW+PajY0426zMkq7/NxUZhCIOhLjZloJjDV1hLDXfZgAnaw6k2ROgkJNjqbcwgNizF57IObU4+L+izLCPT27Z189Dw8oowjvgE0k9t/OqpBEwBh50ig9lh+/KU+JzmaiU5dNU7EddP6RicZeZBLjSrmaa+ZBZYfOTqvzrt6a8JThMaU14hFxwz6sOqiWyFKIzCVe6Hl30OkEdMgXT+r9r76Auxnc/1gY92Gc2bNkc6wQUHnREgHeHx8U8LF8RpxbAKs95SKA9CTFCEL1yX4nfnhyFHvtHuqD1nZ2WhHTWoAEOYxV+egZRxAcss="
- # SONATYPE_PASSWORD
- - secure: "BMBpvpai5dgR5bw0gKDDVkHoyx+USwwqYI1yDAF6QCCdZnJSAzXwSCsfPHYOVUo290YVbC8d7MCEWIE0LmhWflBOd437rLuNvK0MVcCCsDWJwNuYRJgs21ot1BCMw0zLFbFGaGsbcZ5Daau1SdU/lss1UnhVSvNcMs4vs2Hwv8qE7eksVZm4FTtVHsYvDwCO4DgGGAVlQ1VTpsMm5gqdHm5HLiuWm2I67RBoyurAdlroCbyFoxMovE2pHczMJSJXL8z8lMvXm9pJnFyYvm0gQLlzFiKLY7oKvTawolhcXgWJDncMrBcOOAPEDDhlbg5pUZbRdXwlIQN4AQjXVKUDtcFo3OWK2s7d17TQQWrCPrUfhLwyY9Bt3jqw/6gEKxT3bfiEgZCe2/QhYsfIBujgIrzJBKdIXRi0DvxFiExy2LTvQfClTeTZzxmNAVtELw1SDji1j5g8aQQZ1LDIsf84Dxwi0ptYgXsn4aM5v3xJVQ7ksZbSLawxhJflpaxaqlNPcHPm3aa7bLbA3herGR1J6oMlA5NZ+jxaigj1Mb2RVoOa4g1MbyPusZbqvK1oeBr2o72GIL1E2XzVpoSkD+G4qgb65rva1Ujb/XQ3Rojs8qDqelLeH6XfxXD7iSUdhGcBNihrb2Meht/Vr3kH+yFV7RbbD1k08BA5lVBArjhX3yI="
- # SIGNING_KEY_ID
- - secure: "fayTDjzNaVNUtTPZ/8VVWTOb2VgPHj1Kr4o65orYScEjwDkmRSquc9BcamrhWVlURxNeO927Bg9g3LC6Sjmj9jNi7YeGc9nwWuQGEtHLS6EAk8y6aRVg1HN9/gihljTo/syKRdQNO9+ZfMEU4FxCHaGQUg95k8R44Vwm0ijlCoEpSPEsTMMo9PigJ+ln/oQOD+aR7bOu5zSNDi+ZDFqQYr8C3dOYNtFtmyaV7+HLwT7xIdEJfeVSZ+j23wXBLqa6rbIO/SsEZJeTbzY+asBrdh6h7sCIfKPnphNw63dRg0yJed+XVv77HaH4/0iImCySnMoX/dBCxOnf0BArtsujzxjocgaTCmoMoU9QQAzrZl4lfKzPn0+92/sR4Jg6TSZv/vliGWpzCtB+Ic+v28Oe0OjOy8so3feEGDwjjUKNXOovQslo0T84OB/7MJRvAgRLWmcXbjdk8SizNZtr0mfmQ/vVoydZI0s12tLzp/SSAdY7vsw6en9PyCq/LpGSow6tI+lj6HSzHS6KsHeiSr55bRguzytaH7e1SpnsXY31uRT0wvyRi5DOPN8AbJWULKVn82PpC/EBqehRQbd4HuGaEzA5Fan9DxutHEgfZUVZWVnlzUyam+DoO/5zYhcc79UaJYr6IJpGP90gSUQqAJ55fV56jxRvlPrApGNtBUCLy6I="
- # SIGNING_PASSWORD
- - secure: "OP9Y18m1ASYbZN9blGPYf8/nyQeHHUqQDIm/8ZTmpIXWkR+2dA4pcQtMahpFeFduNsju2YbY9dqvzUvfEXwIZF6IeeQ+NJmRCBbdr0vGqx/4HHQY4vt9FZHpsfRcNjE8xmygCYNrQpdgl8VRFdPmcveB54eg3XMrNznDVLSSx/cVrqw+xcPa2162vcs8XziOgglHorJCRz7LAavIEz/hiR9/9FBPTEWCQqnMA1pJn0yfHZtd0ADw+s2tGtB+IJJ8RXYl2fYvS47gK6RISP/QYskXKZgb5qwzbMgaE9lxJ4gXh09D/YCfz1LK+VreVkLAk9PQUQnIpcAggPbRQI+U3ZC/tjEG00L7dGp8a+B8mo3wRWCNgNzr6Mrft5WJ8NEXPiwr1U1FSeM+Zum9ealYqW0wU+ks/79K3GeWZ2ZU/tF1cioh69KbSRtC2zW2s2gBFUgGb63P+7h/QlKDE9tAUBSht4oaYdVALSbvQEYJrWQEZO87+LH0YSaIu+8XuD+OSzgjfWqRWTBUYLKb56iA5978mMtwfQASAwjhQFxdpgHchaM7CUospq09+M3CA0N/OMz9Xr+xgEzej4r5is4bQH1IN87TE6XlKUhy2ZaV98mZ7bQtlPHVlVjslWA8ztgUHKjoK0q3Ob8ot8d3eaqKgAUY6zVoVqrYUf/obYR+yfE="
\ No newline at end of file
diff --git a/README.md b/README.md
index ba4b9e82..64fb134a 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
# Spring REST Docs API specification Integration
+[![oss lifecycle](https://img.shields.io/badge/oss_lifecycle-maintenance-yellow.svg)](https://github.com/ePages-de/restdocs-api-spec/issues/204)
+[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ePages-de_restdocs-api-spec&metric=coverage)](https://sonarcloud.io/summary/new_code?id=ePages-de_restdocs-api-spec)
![](https://img.shields.io/github/license/ePages-de/restdocs-openapi.svg)
-[![Build Status](https://travis-ci.com/ePages-de/restdocs-api-spec.svg?branch=master)](https://travis-ci.com/ePages-de/restdocs-api-spec)
[![Maven Central](https://img.shields.io/maven-central/v/com.epages/restdocs-api-spec)](https://search.maven.org/artifact/com.epages/restdocs-api-spec)
-[![Coverage Status](https://coveralls.io/repos/github/ePages-de/restdocs-api-spec/badge.svg?branch=master)](https://coveralls.io/github/ePages-de/restdocs-api-spec?branch=master)
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/restdocs-api-spec/Lobby)
This is an extension that adds API specifications as an output format to [Spring REST Docs](https://projects.spring.io/spring-restdocs/).
@@ -40,6 +40,7 @@ This is why we came up with this project.
- [Motivation](#motivation)
- [Getting started](#getting-started)
+ - [Version compatibility](#version-compatibility)
- [Project structure](#project-structure)
- [Build configuration](#build-configuration)
- [Gradle](#gradle)
@@ -62,16 +63,24 @@ This is why we came up with this project.
- [OpenAPI 3.0.1](#openapi-301-1)
- [Postman](#postman-1)
- [Generate an HTML-based API reference from OpenAPI](#generate-an-html-based-api-reference-from-openapi)
-- [RAML](#raml)
## Getting started
+### Version compatibility
+
+Spring Boot and Spring REST Docs 3.0.0 introduced [breaking chances to how request parameters are documented: `RequestParameterSnippet` was split into `QueryParameterSnippet` and `FormParameterSnippet`.](https://github.com/spring-projects/spring-restdocs/issues/832)
+
+|Spring Boot version | restdocs-api-spec version|
+|---|---|
+|3.x|0.17.1 or later|
+|2.x|0.16.4|
+
### Project structure
The project consists of the following main components:
- [restdocs-api-spec](restdocs-api-spec) - contains the actual Spring REST Docs extension.
-This is most importantly the [ResourceDocumentation](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceDocumentation.kt) which is the entrypoint to use the extension in your tests.
+This is most importantly the [ResourceDocumentation](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceDocumentation.kt) which is the entry point to use the extension in your tests.
The [ResourceSnippet](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceSnippet.kt) is the snippet used to produce a json file `resource.json` containing all the details about the documented resource.
- [restdocs-api-spec-mockmvc](restdocs-api-spec-mockmvc) - contains a wrapper for `MockMvcRestDocumentation` for easier migration to `restdocs-api-spec` from MockMvc tests that use plain `spring-rest-docs-mockmvc`.
- [restdocs-api-spec-restassured](restdocs-api-spec-restassured) - contains a wrapper for `RestAssuredRestDocumentation` for easier migration to `restdocs-api-spec` from [Rest Assured](http://rest-assured.io) tests that use plain `spring-rest-docs-restassured`.
@@ -85,7 +94,7 @@ The [ResourceSnippet](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apis
* Using the [plugins DSL](https://docs.gradle.org/current/userguide/plugins.html#sec:plugins_block):
```groovy
plugins {
- id 'com.epages.restdocs-api-spec' version '0.15.3'
+ id 'com.epages.restdocs-api-spec' version '0.18.2'
}
```
Examples with Kotlin are also available [here](https://plugins.gradle.org/plugin/com.epages.restdocs-api-spec)
@@ -101,7 +110,7 @@ The [ResourceSnippet](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apis
}
}
dependencies {
- classpath "com.epages:restdocs-api-spec-gradle-plugin:0.15.3" //1.2
+ classpath "com.epages:restdocs-api-spec-gradle-plugin:0.18.2" //1.2
}
}
@@ -111,7 +120,7 @@ The [ResourceSnippet](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apis
2. Add required dependencies to your tests
* *2.1* add the `mavenCentral` repository used to resolve the `com.epages:restdocs-api-spec` module of the project.
* *2.2* add the actual `restdocs-api-spec-mockmvc` dependency to the test scope. Use `restdocs-api-spec-restassured` if you use `RestAssured` instead of `MockMvc`.
- * *2.3* add configuration options for restdocs-api-spec-gradle-plugin`. See [Gradle plugin configuration](#gradle-plugin-configuration)
+ * *2.3* add configuration options for `restdocs-api-spec-gradle-plugin`. See [Gradle plugin configuration](#gradle-plugin-configuration)
```groovy
repositories { //2.1
@@ -120,7 +129,7 @@ The [ResourceSnippet](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apis
dependencies {
//..
- testCompile('com.epages:restdocs-api-spec-mockmvc:0.15.3') //2.2
+ testImplementation('com.epages:restdocs-api-spec-mockmvc:0.18.2') //2.2
}
openapi { //2.3
@@ -261,7 +270,7 @@ Here it is important to add the constraints under the key `validationConstraints
#### MockMvc based tests
-For convenience when applying `restdocs-api-spec` to an existing project that uses Spring REST Docs, we introduced [com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt).
+For convenience when applying `restdocs-api-spec` to an existing project that uses Spring REST Docs, we introduced [com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper](restdocs-api-spec-mockmvc/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt).
In your tests you can just replace calls to `MockMvcRestDocumentation.document` with the corresponding variant of `MockMvcRestDocumentationWrapper.document`.
@@ -273,7 +282,7 @@ Here is an example:
resultActions
.andDo(
MockMvcRestDocumentationWrapper.document(operationName,
- requestFields(fieldDescriptors().getFieldDescriptors()),
+ requestFields(new FieldDescriptors().getFieldDescriptors()),
responseFields(
fieldWithPath("comment").description("the comment"),
fieldWithPath("flag").description("the flag"),
@@ -472,6 +481,10 @@ openapi3 {
title = 'My API title'
version = '1.0.1'
format = 'yaml'
+ contact = {
+ name = 'John Doe'
+ email = 'john.doe@example.com'
+ }
separatePublicApi = true
outputFileNamePrefix = 'my-api-spec'
oauth2SecuritySchemeDefinition = {
@@ -556,52 +569,40 @@ redoc-cli bundle build/api-spec/openapi.json
redoc-cli serve build/api-spec/openapi.json
```
-## RAML
+## Maintenance
-This project supersedes [restdocs-raml](https://github.com/ePages-de/restdocs-raml).
-So if you are coming from `restdocs-raml` you might want to switch to `restdocs-api-spec`.
+This section of the README is targeted at project maintainers.
-The API of both projects is fairly similar and it is easy to migrate.
+### Publish project
-We plan to support RAML in the future.
-In the meantime you can use one of several ways to convert an OpenAPI specification to RAML.
-There are converters around that can help you to achieve this conversion.
+The project is published with the help of [GitHub Actions](./.github/workflows).
+It's version number is determined by the Git tags (see [allegro/axion-release-plugin](https://axion-release-plugin.readthedocs.io)).
+The Java dependencies are published to Sonatype with the help of the [gradle-nexus/publish-plugin](https://github.com/gradle-nexus/publish-plugin) and the Maven Publish Plugin.
+The Gradle plugin is published to the [Gradle plugin portal](https://plugins.gradle.org/plugin/com.epages.restdocs-api-spec) with the help of the ['plugin-publish' plugin](https://plugins.gradle.org/plugin/com.gradle.plugin-publish) (see [docs.gradle.org](https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html)).
-- [oas-raml-converter](https://github.com/mulesoft/oas-raml-converter) - an npm project that provides a CLI to convert between OpenAPI and RAML - it also provides an [online converter](https://mulesoft.github.io/oas-raml-converter/)
-- [api-matic](https://apimatic.io/transformer) - an online converter capable of converting between many api specifications
+Given that the `master` branch on the upstream repository is in the state from which you want to create a release, execute the following steps:
-In the [sample project](samples/restdocs-api-spec-sample) you find a build configuration that uses the [oas-raml-converter-docker](https://hub.docker.com/r/zaddo/oas-raml-converter-docker/) docker image and the [gradle-docker-plugin](https://github.com/bmuschko/gradle-docker-plugin) to leverage the `oas-raml-converter` to convert the output of the `openapi` task to RAML.
-Using this approach your gradle build can still output a RAML specification.
+**(1) Create release**
-See [openapi2raml.gradle](samples/restdocs-api-spec-sample/openapi2raml.gradle).
+[Create release via the GitHub UI](https://github.com/ePages-de/restdocs-api-spec/releases/new).
-```
-./gradlew restdocs-api-spec-sample:openapi
-./gradlew -b samples/restdocs-api-spec-sample/openapi2raml.gradle openapi2raml
-```
+Use the intended version number as "Tag version", e.g. "0.18.2".
+This will automatically trigger a GitHub Action build which publishes the JAR files for this release to Sonatype.
-## Maintenance
+**(2) Login to Sonatype**
-This section of the README is targeted at project maintainers.
+Login to Sonatype and navigate to the [staging repositories](https://oss.sonatype.org/#stagingRepositories).
-### Publish project
+**(3) Close the staging repository**
+
+Select the generated staging repository and close it.
+Check that there are no errors afterwards (e.g. missing signatures or Javadoc JARs).
-The project is published with the help of [TravisCI](./.travis.yml).
-It's version number is determined by the Git tags (see [allegro/axion-release-plugin](https://axion-release-plugin.readthedocs.io)).
-The Java dependencies are published to Sonatype with the help of the [gradle-nexus/publish-plugin](https://github.com/gradle-nexus/publish-plugin) and the Maven Publish Plugin.
-The Gradle plugin is published to the [Gradle plugin portal](https://plugins.gradle.org/plugin/com.epages.restdocs-api-spec) with the help of the ['plugin-publish' plugin](https://plugins.gradle.org/plugin/com.gradle.plugin-publish) (see [docs.gradle.org](https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html)).
+**(4) Release the repository**
-Given that the `master` branch on the upstream repository is in the state from which you want to create a release, execute the following steps:
+Select the generated staging repository and release it.
+After few minutes, the release should be available in the ["Public Repositories" of ePages](https://oss.sonatype.org/service/local/repo_groups/public/content/com/epages/).
+
+**(5) Update documentation**
-1. [Create release via the GitHub UI](https://github.com/ePages-de/restdocs-api-spec/releases/new)
- Use the intended version number as "Tag version", e.g. "0.15.3".
- This will automatically trigger a Travis build which publishes the JAR files for this release to Sonatype.
-2. Login to Sonatype and navigate to the [staging repositories](https://oss.sonatype.org/#stagingRepositories)
-3. Close the staging repository
- Select the generated staging repository and close it.
- Check that there are no errors afterwards (e.g. missing signatures or Javadoc JARs).
-4. Release the repository
- Select the generated staging repository and release it.
- Soon after, the release should be available in the ["Public Repositories" of ePages](https://oss.sonatype.org/service/local/repo_groups/public/content/com/epages/).
-5. Update documentation
- Create a new commit which updates the version numbers in the `README` file.
+Create a new commit which updates the version numbers in the `README` file.
diff --git a/build.gradle.kts b/build.gradle.kts
index 84862f7c..ccf960fd 100755
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,19 +1,18 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-import org.kt3k.gradle.plugin.CoverallsPluginExtension
import pl.allegro.tech.build.axion.release.domain.TagNameSerializationConfig
import pl.allegro.tech.build.axion.release.domain.hooks.HooksConfig
plugins {
- id("com.github.kt3k.coveralls") version "2.8.2"
+ `maven-publish`
id("io.github.gradle-nexus.publish-plugin") version "1.0.0"
id("org.jmailen.kotlinter") version "3.3.0" apply false
+ id("org.sonarqube") version "4.0.0.2929"
id("pl.allegro.tech.build.axion-release") version "1.9.2"
jacoco
java
- kotlin("jvm") version "1.4.20" apply false
- `maven-publish`
+ kotlin("jvm") version "1.7.22" apply false
}
repositories {
@@ -85,12 +84,6 @@ subprojects {
}
}
-//coverall multi module plugin configuration starts here
-configure {
- sourceDirs = nonSampleProjects.flatMap { it.sourceSets["main"].allSource.srcDirs }.filter { it.exists() }.map { it.path }
- jacocoReportPath = "$buildDir/reports/jacoco/jacocoRootReport/jacocoRootReport.xml"
-}
-
tasks {
val jacocoMerge by creating(JacocoMerge::class) {
executionData = files(nonSampleProjects.map { File(it.buildDir, "/jacoco/test.exec") })
@@ -115,7 +108,7 @@ tasks {
xml.isEnabled = true
}
}
- getByName("coveralls").dependsOn(jacocoRootReport)
+ getByName("sonar").dependsOn(jacocoRootReport)
}
nexusPublishing {
@@ -123,3 +116,12 @@ nexusPublishing {
sonatype ()
}
}
+
+sonar {
+ properties {
+ property("sonar.projectKey", "ePages-de_restdocs-api-spec")
+ property("sonar.organization", "epages-de")
+ property("sonar.host.url", "https://sonarcloud.io")
+ property("sonar.exclusions", "**/samples/**")
+ }
+}
diff --git a/ci_build.sh b/ci_build.sh
deleted file mode 100755
index 54317116..00000000
--- a/ci_build.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-set -e
-
-./gradlew clean build coveralls
diff --git a/ci_publish_gradle.sh b/ci_publish_gradle.sh
index bf4a833c..eb636110 100755
--- a/ci_publish_gradle.sh
+++ b/ci_publish_gradle.sh
@@ -1,4 +1,15 @@
#!/bin/bash
set -e
+function check_variable_set() {
+ _VARIABLE_NAME=$1
+ _VARIABLE_VALUE=${!_VARIABLE_NAME}
+ if [[ -z ${_VARIABLE_VALUE} ]]; then
+ echo "Missing env variable ${_VARIABLE_NAME}"
+ exit 1
+ fi
+}
+check_variable_set GRADLE_PUBLISH_KEY
+check_variable_set GRADLE_PUBLISH_SECRET
+
./gradlew publishPlugins -p restdocs-api-spec-gradle-plugin
diff --git a/ci_publish_java.sh b/ci_publish_java.sh
index 2aa6c626..60076d02 100755
--- a/ci_publish_java.sh
+++ b/ci_publish_java.sh
@@ -1,16 +1,102 @@
#!/bin/bash
-set -e
-
-openssl aes-256-cbc -K $encrypted_7b7bcfd5be68_key -iv $encrypted_7b7bcfd5be68_iv \
- -in secret-keys.gpg.enc \
- -out "${SIGNING_KEYRING_FILE}" \
- -d
-
-./gradlew publishToSonatype \
- --info \
- --exclude-task :restdocs-api-spec-gradle-plugin:publishToSonatype \
- -Dorg.gradle.project.sonatypeUsername="${SONATYPE_USERNAME}" \
- -Dorg.gradle.project.sonatypePassword="${SONATYPE_PASSWORD}" \
- -Dorg.gradle.project.signing.keyId="${SIGNING_KEY_ID}" \
- -Dorg.gradle.project.signing.password="${SIGNING_PASSWORD}" \
- -Dorg.gradle.project.signing.secretKeyRingFile="${SIGNING_KEYRING_FILE}"
+
+set -e # Exit with nonzero exit code if anything fails
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+SECRET_KEYS_FILE="${SCRIPT_DIR}/secret-keys.gpg"
+
+###############################################################################
+# Parameter handling
+###############################################################################
+
+usage () {
+ cat << EOF
+DESCRIPTION:
+The script publishes the Java libraries of this project to Sonatype or
+Maven Local (default).
+
+SYNOPSIS:
+$0 [-s] [-h]
+
+OPTIONS:
+ -s Publish to Sonatype (Default: off)
+ -h Show this message.
+ -? Show this message.
+
+REQUIRED ENVIRONMENT VARIABLES:
+- FILE_ENCRYPTION_PASSWORD: Passphrase for decrypting the signing keys
+- SIGNING_KEY_ID
+- SIGNING_PASSWORD
+- SONATYPE_USERNAME
+- SONATYPE_PASSWORD
+
+DEPENDENCIES:
+- gpg: https://help.ubuntu.com/community/GnuPrivacyGuardHowto
+
+EOF
+}
+
+while getopts "s h ?" option ; do
+ case $option in
+ s) PUBLISH_TO_SONATYPE='true'
+ ;;
+ h ) usage
+ exit 0;;
+ ? ) usage
+ exit 0;;
+ esac
+done
+
+
+###############################################################################
+# Env variables and dependencies
+###############################################################################
+
+function check_variable_set() {
+ _VARIABLE_NAME=$1
+ _VARIABLE_VALUE=${!_VARIABLE_NAME}
+ if [[ -z ${_VARIABLE_VALUE} ]]; then
+ echo "Missing env variable ${_VARIABLE_NAME}"
+ exit 1
+ fi
+}
+check_variable_set FILE_ENCRYPTION_PASSWORD
+check_variable_set SIGNING_KEY_ID
+check_variable_set SIGNING_PASSWORD
+check_variable_set SONATYPE_USERNAME
+check_variable_set SONATYPE_PASSWORD
+
+if ! command -v gpg &> /dev/null; then
+ echo "gpg not installed. See https://help.ubuntu.com/community/GnuPrivacyGuardHowto"
+ exit 1
+fi
+
+###############################################################################
+# Parameter handling
+###############################################################################
+
+# Decrypt signing key
+gpg --quiet --batch --yes --decrypt --passphrase="${FILE_ENCRYPTION_PASSWORD}" \
+ --output ${SECRET_KEYS_FILE} secret-keys.gpg.enc
+
+if [[ ! -f "${SECRET_KEYS_FILE}" ]]; then
+ echo "File ${SECRET_KEYS_FILE} does not exist"
+ exit 1
+fi
+
+# Determine where to publish the Java archives
+if [[ "${PUBLISH_TO_SONATYPE}" == "true" ]]; then
+ PUBLISH_GRADLE_TASK="publishToSonatype"
+else
+ PUBLISH_GRADLE_TASK="publishToMavenLocal"
+fi
+
+# Publish
+./gradlew ${PUBLISH_GRADLE_TASK} \
+ --info \
+ --exclude-task :restdocs-api-spec-gradle-plugin:publishToSonatype \
+ -Dorg.gradle.project.sonatypeUsername="${SONATYPE_USERNAME}" \
+ -Dorg.gradle.project.sonatypePassword="${SONATYPE_PASSWORD}" \
+ -Dorg.gradle.project.signing.keyId="${SIGNING_KEY_ID}" \
+ -Dorg.gradle.project.signing.password="${SIGNING_PASSWORD}" \
+ -Dorg.gradle.project.signing.secretKeyRingFile="${SECRET_KEYS_FILE}"
diff --git a/ci_test.sh b/ci_test.sh
new file mode 100755
index 00000000..52defd74
--- /dev/null
+++ b/ci_test.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+set -e # Exit with nonzero exit code if anything fails
+
+if [[ -n "${SONAR_TOKEN}" ]]; then
+ SONAR_GRADLE_TASK="sonar"
+else
+ echo "INFO: Skipping sonar analysis as SONAR_TOKEN is not set"
+fi
+
+./gradlew \
+ clean \
+ ${SONAR_GRADLE_TASK} \
+ build \
+ --info
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..f9683961
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1 @@
+org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m -Xms256m -Xmx512m
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e708b1c0..41d9927a 100755
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 442d9132..92f06b50 100755
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 4f906e0c..1b6c7873 100755
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
-#!/usr/bin/env sh
+#!/bin/sh
#
-# Copyright 2015 the original author or authors.
+# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,67 +17,101 @@
#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
+APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
+ JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +140,95 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=`expr $i + 1`
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- 0) set -- ;;
- 1) set -- "$args0" ;;
- 2) set -- "$args0" "$args1" ;;
- 3) set -- "$args0" "$args1" "$args2" ;;
- 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
exec "$JAVACMD" "$@"
diff --git a/restdocs-api-spec-gradle-plugin/build.gradle.kts b/restdocs-api-spec-gradle-plugin/build.gradle.kts
index 43c38967..0687ef50 100755
--- a/restdocs-api-spec-gradle-plugin/build.gradle.kts
+++ b/restdocs-api-spec-gradle-plugin/build.gradle.kts
@@ -45,8 +45,8 @@ val jacocoRuntime by configurations.creating
dependencies {
compileOnly(gradleKotlinDsl())
- compile(kotlin("gradle-plugin"))
- compile(kotlin("stdlib-jdk8"))
+ implementation(kotlin("gradle-plugin"))
+ implementation(kotlin("stdlib-jdk8"))
implementation(project(":restdocs-api-spec-openapi-generator"))
implementation(project(":restdocs-api-spec-openapi3-generator"))
@@ -60,7 +60,7 @@ dependencies {
testImplementation("com.jayway.jsonpath:json-path:2.4.0")
- testCompile(gradleTestKit())
+ testImplementation(gradleTestKit())
jacocoRuntime("org.jacoco:org.jacoco.agent:0.8.2:runtime")
}
diff --git a/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApi3Task.kt b/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApi3Task.kt
index 4b290029..d19d396e 100644
--- a/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApi3Task.kt
+++ b/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApi3Task.kt
@@ -2,6 +2,7 @@ package com.epages.restdocs.apispec.gradle
import com.epages.restdocs.apispec.model.ResourceModel
import com.epages.restdocs.apispec.openapi3.OpenApi3Generator
+import io.swagger.v3.oas.models.info.Contact
import io.swagger.v3.oas.models.servers.Server
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
@@ -12,9 +13,14 @@ open class OpenApi3Task : OpenApiBaseTask() {
@Optional
var servers: List = listOf()
+ @Input
+ @Optional
+ var contact: Contact? = null
+
fun applyExtension(extension: OpenApi3Extension) {
super.applyExtension(extension)
servers = extension.servers
+ contact = extension.contact
}
override fun generateSpecification(resourceModels: List): String {
@@ -26,7 +32,8 @@ open class OpenApi3Task : OpenApiBaseTask() {
tagDescriptions = tagDescriptions,
version = apiVersion,
oauth2SecuritySchemeDefinition = oauth2SecuritySchemeDefinition,
- format = format
+ format = format,
+ contact = contact
)
}
}
diff --git a/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiExtension.kt b/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiExtension.kt
index d508ff35..93d36eee 100644
--- a/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiExtension.kt
+++ b/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiExtension.kt
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.readValue
import groovy.lang.Closure
+import io.swagger.v3.oas.models.info.Contact
import io.swagger.v3.oas.models.servers.Server
import org.gradle.api.Project
import java.io.File
@@ -63,6 +64,7 @@ open class OpenApi3Extension(project: Project) : OpenApiBaseExtension(project) {
override var outputFileNamePrefix = "openapi3"
private var _servers: List = mutableListOf(Server().apply { url = "http://localhost" })
+ private var _contact: Contact? = null
val servers
get() = _servers
@@ -79,6 +81,13 @@ open class OpenApi3Extension(project: Project) : OpenApiBaseExtension(project) {
_servers = serversActions.map { project.configure(Server(), it) as Server }
}
+ val contact
+ get() = _contact
+
+ fun setContact(contact: Closure) {
+ _contact = project.configure(Contact(), contact) as Contact
+ }
+
companion object {
const val name = "openapi3"
}
diff --git a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApi3TaskTest.kt b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApi3TaskTest.kt
index 2f4ca7bf..fa7ef77d 100644
--- a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApi3TaskTest.kt
+++ b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApi3TaskTest.kt
@@ -37,6 +37,18 @@ class RestdocsOpenApi3TaskTest : RestdocsOpenApiTaskTestBase() {
thenSingleServerContainedInOutput()
}
+ @Test
+ fun `should run openapi task with contact`() {
+ givenBuildFileWithOpenApiClosureWithContact()
+ givenResourceSnippet()
+
+ whenPluginExecuted()
+
+ thenApiSpecTaskSuccessful()
+ thenOutputFileFound()
+ thenContactContainedInOutput()
+ }
+
@Test
fun `should run openapi task with single server string`() {
givenBuildFileWithOpenApiClosureWithSingleServerString()
@@ -67,6 +79,12 @@ class RestdocsOpenApi3TaskTest : RestdocsOpenApiTaskTestBase() {
}
}
+ private fun thenContactContainedInOutput() {
+ with(outputFileContext()) {
+ then(read("info.contact.name")).isEqualTo("Test Contact")
+ }
+ }
+
private fun thenHeaderWithDefaultValuesContainedInOutput() {
with(outputFileContext()) {
then(read("paths./products/{id}.get.parameters[1].name")).isEqualTo("one")
@@ -86,6 +104,10 @@ class RestdocsOpenApi3TaskTest : RestdocsOpenApiTaskTestBase() {
givenBuildFileWithOpenApiClosure("server", """{ url = 'http://some.api' }""")
}
+ fun givenBuildFileWithOpenApiClosureWithContact() {
+ givenBuildFileWithOpenApiClosure("contact", """{ name = 'Test Contact' }""")
+ }
+
override fun givenBuildFileWithOpenApiClosure() {
givenBuildFileWithOpenApiClosure(
"servers",
@@ -136,6 +158,7 @@ class RestdocsOpenApi3TaskTest : RestdocsOpenApiTaskTestBase() {
baseBuildFile() + """
openapi3 {
servers = [ { url = "http://some.api" } ]
+ contact = { name = "Test Contact" }
title = '$title'
description = '$description'
tagDescriptionsPropertiesFile = "tagDescriptions.yaml"
diff --git a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApiTaskTest.kt b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApiTaskTest.kt
index 578f1f38..a10cd4c0 100644
--- a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApiTaskTest.kt
+++ b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApiTaskTest.kt
@@ -54,11 +54,11 @@ class RestdocsOpenApiTaskTest : RestdocsOpenApiTaskTestBase() {
override fun thenSecurityDefinitionsFoundInOutputFile() {
with(JsonPath.parse(outputFolder.resolve("$outputFileNamePrefix.$format").readText())) {
- then(read("securityDefinitions.oauth2_accessCode.scopes.prod:r")).isEqualTo("Some text")
- then(read("securityDefinitions.oauth2_accessCode.type")).isEqualTo("oauth2")
- then(read("securityDefinitions.oauth2_accessCode.tokenUrl")).isNotEmpty()
- then(read("securityDefinitions.oauth2_accessCode.authorizationUrl")).isNotEmpty()
- then(read("securityDefinitions.oauth2_accessCode.flow")).isNotEmpty()
+ then(read("securityDefinitions.oauth2.scopes.prod:r")).isEqualTo("Some text")
+ then(read("securityDefinitions.oauth2.type")).isEqualTo("oauth2")
+ then(read("securityDefinitions.oauth2.tokenUrl")).isNotEmpty()
+ then(read("securityDefinitions.oauth2.authorizationUrl")).isNotEmpty()
+ then(read("securityDefinitions.oauth2.flow")).isNotEmpty()
}
}
}
diff --git a/restdocs-api-spec-jsonschema/build.gradle.kts b/restdocs-api-spec-jsonschema/build.gradle.kts
index 66a178b0..6c3b4f4d 100644
--- a/restdocs-api-spec-jsonschema/build.gradle.kts
+++ b/restdocs-api-spec-jsonschema/build.gradle.kts
@@ -13,17 +13,17 @@ val jacksonVersion: String by extra
val junitVersion: String by extra
dependencies {
- compile(kotlin("stdlib-jdk8"))
- compile(project(":restdocs-api-spec-model"))
- compile("com.github.erosb:everit-json-schema:1.11.0")
- compile("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
- compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
+ implementation(kotlin("stdlib-jdk8"))
+ implementation(project(":restdocs-api-spec-model"))
+ implementation("com.github.erosb:everit-json-schema:1.11.0")
+ implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
- testCompile("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
- testCompile("com.github.java-json-tools:json-schema-validator:2.2.10")
- testCompile("com.jayway.jsonpath:json-path:2.4.0")
- testCompile("org.assertj:assertj-core:3.10.0")
- testCompile("javax.validation:validation-api:2.0.1.Final")
+ testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
+ testImplementation("com.github.java-json-tools:json-schema-validator:2.2.10")
+ testImplementation("com.jayway.jsonpath:json-path:2.4.0")
+ testImplementation("org.assertj:assertj-core:3.10.0")
+ testImplementation("javax.validation:validation-api:2.0.1.Final")
}
publishing {
diff --git a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/ConstraintResolver.kt b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/ConstraintResolver.kt
index a3a8ab3f..982ab01f 100644
--- a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/ConstraintResolver.kt
+++ b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/ConstraintResolver.kt
@@ -36,7 +36,7 @@ internal object ConstraintResolver {
private fun FieldDescriptor.maybeSizeConstraint() = findConstraints(this).firstOrNull { SIZE_CONSTRAINT == it.name }
- internal fun maybePattern(fieldDescriptor: FieldDescriptor?) = fieldDescriptor?.maybePatternConstraint()?.let { it.configuration["pattern"] as? String }
+ internal fun maybePattern(fieldDescriptor: FieldDescriptor?) = fieldDescriptor?.maybePatternConstraint()?.let { it.configuration["regexp"] as? String }
private fun FieldDescriptor.maybePatternConstraint() = findConstraints(this).firstOrNull { PATTERN_CONSTRAINT == it.name }
@@ -82,9 +82,10 @@ internal object ConstraintResolver {
.minOrNull()
}
- internal fun isRequired(fieldDescriptor: FieldDescriptor): Boolean =
- findConstraints(fieldDescriptor)
- .any { constraint -> REQUIRED_CONSTRAINTS.contains(constraint.name) }
+ internal fun isRequired(fieldDescriptor: FieldDescriptor): Boolean = findConstraints(fieldDescriptor)
+ .any { constraint ->
+ REQUIRED_CONSTRAINTS.contains(constraint.name)
+ } || !fieldDescriptor.optional
private fun findConstraints(fieldDescriptor: FieldDescriptor): List =
fieldDescriptor.attributes.validationConstraints
diff --git a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt
index 569a6123..56f7adb6 100644
--- a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt
+++ b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt
@@ -15,6 +15,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.everit.json.schema.ArraySchema
import org.everit.json.schema.BooleanSchema
import org.everit.json.schema.CombinedSchema
+import org.everit.json.schema.CombinedSchema.oneOf
import org.everit.json.schema.EmptySchema
import org.everit.json.schema.EnumSchema
import org.everit.json.schema.NullSchema
@@ -167,11 +168,13 @@ class JsonSchemaFromFieldDescriptorsGenerator {
.build()
)
} else {
+ val schemaName = propertyField?.fieldDescriptor?.attributes?.schemaName
builder.addPropertySchema(
propertyName,
traverse(
traversedSegments, fields,
ObjectSchema.builder()
+ .title(schemaName)
.description(propertyField?.fieldDescriptor?.description) as ObjectSchema.Builder
)
)
@@ -206,9 +209,26 @@ class JsonSchemaFromFieldDescriptorsGenerator {
) : FieldDescriptor(path, description, type, optional, ignored, attributes) {
fun jsonSchemaType(): Schema {
- val schemaBuilders = jsonSchemaPrimitiveTypes.map { typeToSchema(it) }
- return if (schemaBuilders.size == 1) schemaBuilders.first().description(description).build()
- else CombinedSchema.oneOf(schemaBuilders.map { it.build() }).description(description).build()
+ val schemaBuilders: List>
+ if (jsonSchemaPrimitiveTypes.size > 1 &&
+ optional &&
+ !jsonSchemaPrimitiveTypes.contains("null")
+ ) {
+ schemaBuilders = jsonSchemaPrimitiveTypes
+ .plus(jsonSchemaPrimitiveTypeFromDescriptorType("null"))
+ .map { typeToSchema(it) }
+ } else {
+ schemaBuilders = jsonSchemaPrimitiveTypes.map { typeToSchema(it) }
+ }
+ return if (schemaBuilders.size == 1) schemaBuilders.first().description(description).checkNullable().build()
+ else oneOf(schemaBuilders.map { it.build() }).description(description).checkNullable().build()
+ }
+
+ private fun Schema.Builder.checkNullable(): Schema.Builder {
+ if (optional) {
+ this.nullable(true)
+ }
+ return this
}
fun merge(fieldDescriptor: FieldDescriptor): FieldDescriptorWithSchemaType {
@@ -230,7 +250,9 @@ class JsonSchemaFromFieldDescriptorsGenerator {
private fun typeToSchema(type: String): Schema.Builder<*> =
when (type) {
- "null" -> NullSchema.builder()
+ "null" -> {
+ NullSchema.builder().nullable()
+ }
"empty" -> EmptySchema.builder()
"object" -> ObjectSchema.builder()
"array" -> ArraySchema.builder().applyConstraints(this).allItemSchema(arrayItemsSchema())
@@ -246,9 +268,14 @@ class JsonSchemaFromFieldDescriptorsGenerator {
else -> throw IllegalArgumentException("unknown field type $type")
}
+ private fun NullSchema.Builder.nullable(): NullSchema.Builder {
+ this.nullable(true)
+ return this
+ }
+
private fun arrayItemsSchema(): Schema {
return attributes.itemsType
- ?.let { typeToSchema(it.toLowerCase()).build() }
+ ?.let { typeToSchema(it.lowercase()).build() }
?: CombinedSchema.oneOf(
listOf(
ObjectSchema.builder().build(),
@@ -277,7 +304,7 @@ class JsonSchemaFromFieldDescriptorsGenerator {
)
private fun jsonSchemaPrimitiveTypeFromDescriptorType(fieldDescriptorType: String) =
- fieldDescriptorType.toLowerCase()
+ fieldDescriptorType.lowercase()
.let { if (it == "varies") "empty" else it } // varies is used by spring rest docs if the type is ambiguous - in json schema we want to represent as empty
}
}
diff --git a/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt b/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt
index f0c7b1a1..a9ed3b12 100644
--- a/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt
+++ b/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt
@@ -19,6 +19,7 @@ import org.everit.json.schema.Schema
import org.everit.json.schema.StringSchema
import org.everit.json.schema.ValidationException
import org.everit.json.schema.loader.SchemaLoader
+import org.everit.json.schema.loader.internal.DefaultSchemaClient
import org.json.JSONArray
import org.json.JSONObject
import org.junit.jupiter.api.Test
@@ -39,6 +40,22 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
private var schemaString: String? = null
+ @Test
+ @Throws(IOException::class)
+ fun should_generate_reuse_schema() {
+ givenFieldDescriptorsWithSchemaName()
+
+ whenSchemaGenerated()
+
+ then(schema).isInstanceOf(ObjectSchema::class.java)
+ val objectSchema = schema as ObjectSchema?
+ val postSchema = objectSchema?.propertySchemas?.get("post") as ObjectSchema
+ val shippingAddressSchema = postSchema.propertySchemas["shippingAddress"] as ObjectSchema
+ then(shippingAddressSchema.title).isEqualTo("Address")
+ val billingAddressSchema = postSchema.propertySchemas["billingAddress"] as ObjectSchema
+ then(billingAddressSchema.title).isEqualTo("Address")
+ }
+
@Test
@Throws(IOException::class)
fun should_generate_complex_schema() {
@@ -61,6 +78,7 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
val shippingAddressSchema = objectSchema.propertySchemas["shippingAddress"]!!
then(shippingAddressSchema).isInstanceOf(ObjectSchema::class.java)
then(shippingAddressSchema.description).isNotEmpty()
+ then(shippingAddressSchema.isNullable).isTrue()
then(objectSchema.definesProperty("billingAddress")).isTrue()
val billingAddressSchema = objectSchema.propertySchemas["billingAddress"] as ObjectSchema
@@ -120,6 +138,7 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
then(pagePositiveSchema.minimum.toInt()).isEqualTo(1)
then(pagePositiveSchema.maximum).isNull()
then(pagePositiveSchema.requiresInteger()).isTrue
+ then(pagePositiveSchema.isNullable).isTrue()
then(objectSchema.definesProperty("page100_200")).isTrue
then(objectSchema.propertySchemas["page100_200"]).isInstanceOf(NumberSchema::class.java)
@@ -445,7 +464,14 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
private fun whenSchemaGenerated() {
schemaString = generator.generateSchema(fieldDescriptors!!)
println(schemaString)
- schema = SchemaLoader.load(JSONObject(schemaString))
+ schema = SchemaLoader
+ .builder()
+ .nullableSupport(true)
+ .schemaJson(JSONObject(schemaString))
+ .schemaClient(DefaultSchemaClient())
+ .build()
+ .load()
+ .build()
}
private fun givenFieldDescriptorWithPrimitiveArray() {
@@ -589,9 +615,9 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
private fun givenDifferentFieldDescriptorsWithSamePathAndDifferentTypes() {
fieldDescriptors = listOf(
- FieldDescriptor("id", "some", "STRING"),
- FieldDescriptor("id", "some", "NULL"),
- FieldDescriptor("id", "some", "BOOLEAN")
+ FieldDescriptor("id", "some", "STRING", true),
+ FieldDescriptor("id", "some", "NULL", true),
+ FieldDescriptor("id", "some", "BOOLEAN", true)
)
}
@@ -624,7 +650,7 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
listOf(
Constraint(
"javax.validation.constraints.Pattern",
- mapOf("pattern" to "[a-z]")
+ mapOf("regexp" to "[a-z]")
)
)
)
@@ -655,7 +681,7 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
),
FieldDescriptor("lineItems[*].quantity.unit", "some", "STRING"),
- FieldDescriptor("shippingAddress", "some", "OBJECT"),
+ FieldDescriptor("shippingAddress", "some", "OBJECT", true),
FieldDescriptor("billingAddress", "some", "OBJECT"),
FieldDescriptor(
"billingAddress.firstName", "some", "STRING",
@@ -732,6 +758,7 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
"pagePositive",
"some",
"NUMBER",
+ true,
attributes = Attributes(
listOf(
Constraint(
@@ -778,6 +805,23 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
)
}
+ private fun givenFieldDescriptorsWithSchemaName() {
+
+ fieldDescriptors = listOf(
+ FieldDescriptor(
+ "post",
+ "some",
+ "OBJECT",
+ ),
+ FieldDescriptor("post.shippingAddress", "some", "OBJECT", attributes = Attributes(schemaName = "Address")),
+ FieldDescriptor("post.shippingAddress.firstName", "some", "STRING"),
+ FieldDescriptor("post.shippingAddress.valid", "some", "BOOLEAN"),
+ FieldDescriptor("post.billingAddress", "some", "OBJECT", attributes = Attributes(schemaName = "Address")),
+ FieldDescriptor("post.billingAddress.firstName", "some", "STRING"),
+ FieldDescriptor("post.billingAddress.valid", "some", "BOOLEAN"),
+ )
+ }
+
private fun thenSchemaValidatesJson(json: String) {
schema!!.validate(if (json.startsWith("[")) JSONArray(json) else JSONObject(json))
}
diff --git a/restdocs-api-spec-mockmvc/build.gradle.kts b/restdocs-api-spec-mockmvc/build.gradle.kts
index a8d1546b..0f207f94 100644
--- a/restdocs-api-spec-mockmvc/build.gradle.kts
+++ b/restdocs-api-spec-mockmvc/build.gradle.kts
@@ -12,17 +12,17 @@ val springRestDocsVersion: String by extra
val junitVersion: String by extra
dependencies {
- compile(kotlin("stdlib-jdk8"))
+ implementation(kotlin("stdlib-jdk8"))
- compile(project(":restdocs-api-spec"))
- compile("org.springframework.restdocs:spring-restdocs-mockmvc:$springRestDocsVersion")
+ api(project(":restdocs-api-spec"))
+ implementation("org.springframework.restdocs:spring-restdocs-mockmvc:$springRestDocsVersion")
- testCompile("org.springframework.boot:spring-boot-starter-test:$springBootVersion") {
+ testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") {
exclude("junit")
}
- testCompile("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
+ testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
testImplementation("org.junit-pioneer:junit-pioneer:0.3.0")
- testCompile("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion")
+ testImplementation("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion")
}
publishing {
diff --git a/restdocs-api-spec-model/build.gradle.kts b/restdocs-api-spec-model/build.gradle.kts
index d9aa3c49..86d5131b 100644
--- a/restdocs-api-spec-model/build.gradle.kts
+++ b/restdocs-api-spec-model/build.gradle.kts
@@ -12,7 +12,7 @@ repositories {
}
dependencies {
- compile(kotlin("stdlib-jdk8"))
+ implementation(kotlin("stdlib-jdk8"))
implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion")
}
diff --git a/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/Oauth2Configuration.kt b/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/Oauth2Configuration.kt
index a754409e..85e87ea9 100644
--- a/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/Oauth2Configuration.kt
+++ b/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/Oauth2Configuration.kt
@@ -6,5 +6,5 @@ open class Oauth2Configuration(
var flows: Array = arrayOf(),
var scopes: Map = mapOf()
) {
- fun securitySchemeName(flow: String) = "oauth2_$flow"
+ fun securitySchemeName() = "oauth2"
}
diff --git a/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt b/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt
index c7605f9e..7d53a63d 100644
--- a/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt
+++ b/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt
@@ -90,7 +90,8 @@ open class FieldDescriptor(
data class Attributes(
val validationConstraints: List = emptyList(),
val enumValues: List = emptyList(),
- val itemsType: String? = null
+ val itemsType: String? = null,
+ val schemaName: String? = null,
)
data class Constraint(
diff --git a/restdocs-api-spec-openapi-generator/build.gradle.kts b/restdocs-api-spec-openapi-generator/build.gradle.kts
index a29d261e..def92a93 100644
--- a/restdocs-api-spec-openapi-generator/build.gradle.kts
+++ b/restdocs-api-spec-openapi-generator/build.gradle.kts
@@ -10,13 +10,13 @@ repositories {
val junitVersion: String by extra
dependencies {
- compile(kotlin("stdlib-jdk8"))
+ implementation(kotlin("stdlib-jdk8"))
- compile(project(":restdocs-api-spec-model"))
- compile(project(":restdocs-api-spec-jsonschema"))
- compile("io.swagger:swagger-core:1.5.22")
- compile("com.fasterxml.jackson.core:jackson-databind:2.12.2")
- compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.2")
+ api(project(":restdocs-api-spec-model"))
+ api(project(":restdocs-api-spec-jsonschema"))
+ api("io.swagger:swagger-core:1.5.22")
+ implementation("com.fasterxml.jackson.core:jackson-databind:2.12.2")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.2")
testImplementation("io.swagger:swagger-parser:1.0.36")
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
diff --git a/restdocs-api-spec-openapi-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20Generator.kt b/restdocs-api-spec-openapi-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20Generator.kt
index 3dfe5480..f888420d 100644
--- a/restdocs-api-spec-openapi-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20Generator.kt
+++ b/restdocs-api-spec-openapi-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20Generator.kt
@@ -40,6 +40,7 @@ object OpenApi20Generator {
private const val API_KEY_SECURITY_NAME = "api_key"
private const val BASIC_SECURITY_NAME = "basic"
+ private const val OAUTH2_SECURITY_NAME = "oauth2"
private val PATH_PARAMETER_PATTERN = """\{([^/}]+)}""".toRegex()
internal fun generate(
resources: List,
@@ -323,16 +324,10 @@ object OpenApi20Generator {
val securityRequirements = firstModelForPathAndMethod.request.securityRequirements
if (securityRequirements != null) {
when (securityRequirements.type) {
- SecurityType.OAUTH2 -> oauth2SecuritySchemeDefinition?.flows?.map {
- addSecurity(
- oauth2SecuritySchemeDefinition.securitySchemeName(it),
- securityRequirements2ScopesList(
- securityRequirements
- )
- )
- }
+ SecurityType.OAUTH2 -> addSecurity(OAUTH2_SECURITY_NAME, securityRequirements2ScopesList(securityRequirements))
SecurityType.BASIC -> addSecurity(BASIC_SECURITY_NAME, null)
SecurityType.API_KEY -> addSecurity(API_KEY_SECURITY_NAME, null)
+ SecurityType.JWT_BEARER -> { /* not specified for OpenApi 2.0 */ }
}
}
}
@@ -372,7 +367,7 @@ object OpenApi20Generator {
addScope(it, scopeAndDescriptions.getOrDefault(it, "No description"))
}
}
- openApi.addSecurityDefinition(oauth2SecuritySchemeDefinition.securitySchemeName(flow), oauth2Definition)
+ openApi.addSecurityDefinition(oauth2SecuritySchemeDefinition.securitySchemeName(), oauth2Definition)
}
if (hasAnyOperationWithSecurityName(
openApi,
diff --git a/restdocs-api-spec-openapi-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20GeneratorTest.kt b/restdocs-api-spec-openapi-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20GeneratorTest.kt
index 5a587f36..251fd4a0 100644
--- a/restdocs-api-spec-openapi-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20GeneratorTest.kt
+++ b/restdocs-api-spec-openapi-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20GeneratorTest.kt
@@ -141,8 +141,8 @@ class OpenApi20GeneratorTest {
val openapi = whenOpenApiObjectGenerated(api)
with(openapi.securityDefinitions) {
- then(this.containsKey("oauth2_accessCode"))
- then(this["oauth2_accessCode"])
+ then(this.containsKey("oauth2"))
+ then(this["oauth2"])
.isEqualToComparingFieldByField(
OAuth2Definition().accessCode("http://example.com/authorize", "http://example.com/token")
.apply { addScope("prod:r", "No description") }
@@ -356,12 +356,12 @@ class OpenApi20GeneratorTest {
then(productPath.get.operationId).isNotEmpty()
then(productPath.get.consumes).contains(successfulGetProductModel.request.contentType)
- then(productPath.get.security).hasSize(2)
+ then(productPath.get.security).hasSize(1)
then(productPath.get.tags).containsOnly("tag1", "tag2")
val combined = productPath.get.security.reduce { map1, map2 -> map1 + map2 }
- then(combined).containsOnlyKeys("oauth2_application", "oauth2_accessCode")
+ then(combined).containsOnlyKeys("oauth2")
then(combined.values).containsOnly(listOf("prod:r"))
then(successfulGetResponse).isNotNull
diff --git a/restdocs-api-spec-openapi3-generator/build.gradle.kts b/restdocs-api-spec-openapi3-generator/build.gradle.kts
index d78e574c..9046640c 100644
--- a/restdocs-api-spec-openapi3-generator/build.gradle.kts
+++ b/restdocs-api-spec-openapi3-generator/build.gradle.kts
@@ -11,14 +11,14 @@ val jacksonVersion: String by extra
val junitVersion: String by extra
dependencies {
- compile(kotlin("stdlib-jdk8"))
+ implementation(kotlin("stdlib-jdk8"))
- compile(project(":restdocs-api-spec-model"))
- compile(project(":restdocs-api-spec-jsonschema"))
+ api(project(":restdocs-api-spec-model"))
+ api(project(":restdocs-api-spec-jsonschema"))
- compile("io.swagger.core.v3:swagger-core:2.1.3")
- compile("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
- compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
+ api("io.swagger.core.v3:swagger-core:2.1.3")
+ implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
testImplementation("io.swagger:swagger-parser:2.0.0-rc1")
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
diff --git a/restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3Generator.kt b/restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3Generator.kt
index 77ad287b..a1f6b09b 100644
--- a/restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3Generator.kt
+++ b/restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3Generator.kt
@@ -14,6 +14,7 @@ import com.epages.restdocs.apispec.model.SimpleType
import com.epages.restdocs.apispec.model.groupByPath
import com.epages.restdocs.apispec.openapi3.SecuritySchemeGenerator.addSecurityDefinitions
import com.epages.restdocs.apispec.openapi3.SecuritySchemeGenerator.addSecurityItemFromSecurityRequirements
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import io.swagger.v3.core.util.Json
import io.swagger.v3.oas.models.Components
@@ -23,6 +24,7 @@ import io.swagger.v3.oas.models.PathItem
import io.swagger.v3.oas.models.Paths
import io.swagger.v3.oas.models.examples.Example
import io.swagger.v3.oas.models.headers.Header
+import io.swagger.v3.oas.models.info.Contact
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.media.BooleanSchema
import io.swagger.v3.oas.models.media.Content
@@ -51,7 +53,8 @@ object OpenApi3Generator {
description: String? = null,
tagDescriptions: Map = emptyMap(),
version: String = "1.0.0",
- oauth2SecuritySchemeDefinition: Oauth2Configuration? = null
+ oauth2SecuritySchemeDefinition: Oauth2Configuration? = null,
+ contact: Contact? = null
): OpenAPI {
return OpenAPI().apply {
@@ -60,6 +63,7 @@ object OpenApi3Generator {
this.title = title
this.description = description
this.version = version
+ this.contact = contact
}
this.tags(
tagDescriptions.map {
@@ -73,11 +77,41 @@ object OpenApi3Generator {
resources,
oauth2SecuritySchemeDefinition
)
+
extractDefinitions()
+ makeSubSchema()
addSecurityDefinitions(oauth2SecuritySchemeDefinition)
}
}
+ private fun OpenAPI.makeSubSchema() {
+ val schemas = this.components.schemas
+ val subSchemas = mutableMapOf>()
+ schemas.forEach {
+ val schema = it.value
+ if (schema.properties != null) {
+ makeSubSchema(subSchemas, schema.properties)
+ }
+ }
+
+ if (subSchemas.isNotEmpty()) {
+ this.components.schemas.putAll(subSchemas)
+ }
+ }
+
+ private fun makeSubSchema(schemas: MutableMap>, properties: Map>) {
+ properties.asSequence().filter { it.value.title != null }.forEach {
+ val objectMapper = jacksonObjectMapper()
+ val subSchema = it.value
+ val strSubSchema = objectMapper.writeValueAsString(subSchema)
+ val copySchema = objectMapper.readValue(strSubSchema, subSchema.javaClass)
+ val schemaTitle = copySchema.title
+ subSchema.`$ref`("#/components/schemas/$schemaTitle")
+ schemas[schemaTitle] = copySchema
+ makeSubSchema(schemas, copySchema.properties)
+ }
+ }
+
fun generateAndSerialize(
resources: List,
servers: List,
@@ -86,7 +120,8 @@ object OpenApi3Generator {
tagDescriptions: Map = emptyMap(),
version: String = "1.0.0",
oauth2SecuritySchemeDefinition: Oauth2Configuration? = null,
- format: String
+ format: String,
+ contact: Contact? = null
) =
ApiSpecificationWriter.serialize(
format,
@@ -97,7 +132,8 @@ object OpenApi3Generator {
description = description,
tagDescriptions = tagDescriptions,
version = version,
- oauth2SecuritySchemeDefinition = oauth2SecuritySchemeDefinition
+ oauth2SecuritySchemeDefinition = oauth2SecuritySchemeDefinition,
+ contact = contact
)
)
@@ -127,6 +163,8 @@ object OpenApi3Generator {
schemasToKeys.getValue(it) to it
}.toMap()
}
+
+ this.components
}
private fun List.extractSchemas(
@@ -262,7 +300,7 @@ object OpenApi3Generator {
)
}
)
- }.apply { addSecurityItemFromSecurityRequirements(firstModelForPathAndMethod.request.securityRequirements, oauth2SecuritySchemeDefinition) }
+ }.apply { addSecurityItemFromSecurityRequirements(firstModelForPathAndMethod.request.securityRequirements) }
}
private fun operationId(operationIds: List): String {
@@ -449,30 +487,28 @@ object OpenApi3Generator {
.map { it as Boolean }
.forEach { this.addEnumItem(it) }
}
+
SimpleType.STRING.name.toLowerCase() -> StringSchema().apply {
this._default(parameterDescriptor.defaultValue?.let { it as String })
parameterDescriptor.attributes.enumValues
.map { it as String }
.forEach { this.addEnumItem(it) }
}
+
SimpleType.NUMBER.name.toLowerCase() -> NumberSchema().apply {
- this._default(parameterDescriptor.defaultValue?.let { it as BigDecimal })
+ this._default(parameterDescriptor.defaultValue?.asBigDecimal())
parameterDescriptor.attributes.enumValues
- .map {
- when (it) {
- is Int -> it.toBigDecimal()
- is Double -> it.toBigDecimal()
- else -> it as BigDecimal
- }
- }
+ .map { it.asBigDecimal() }
.forEach { this.addEnumItem(it) }
}
+
SimpleType.INTEGER.name.toLowerCase() -> IntegerSchema().apply {
- this._default(parameterDescriptor.defaultValue?.let { it as Int })
+ this._default(parameterDescriptor.defaultValue?.asInt())
parameterDescriptor.attributes.enumValues
- .map { it as Int }
+ .map { it.asInt() }
.forEach { this.addEnumItem(it) }
}
+
else -> throw IllegalArgumentException("Unknown type '${parameterDescriptor.type}'")
}
}
@@ -485,6 +521,24 @@ object OpenApi3Generator {
return if (this.isEmpty()) null else this
}
+ private fun Any.asInt(): Int {
+ return when (this) {
+ is Int -> this
+ is Long -> toInt()
+ else -> this as Int
+ }
+ }
+
+ private fun Any.asBigDecimal(): BigDecimal {
+ return when (this) {
+ is Int -> toBigDecimal()
+ is Long -> toBigDecimal()
+ is Double -> toBigDecimal()
+ is Float -> toBigDecimal()
+ else -> this as BigDecimal
+ }
+ }
+
private data class RequestModelWithOperationId(
val operationId: String,
val request: RequestModel
diff --git a/restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/SecuritySchemeGenerator.kt b/restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/SecuritySchemeGenerator.kt
index d4d4b2ae..a2dd5d84 100644
--- a/restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/SecuritySchemeGenerator.kt
+++ b/restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/SecuritySchemeGenerator.kt
@@ -16,12 +16,13 @@ internal object SecuritySchemeGenerator {
private const val API_KEY_SECURITY_NAME = "api_key"
private const val BASIC_SECURITY_NAME = "basic"
private const val JWT_BEARER_SECURITY_NAME = "bearerAuthJWT"
+ private const val OAUTH2_SECURITY_NAME = "oauth2"
fun OpenAPI.addSecurityDefinitions(oauth2SecuritySchemeDefinition: Oauth2Configuration?) {
if (oauth2SecuritySchemeDefinition?.flows?.isNotEmpty() == true) {
val flows = OAuthFlows()
components.addSecuritySchemes(
- "oauth2",
+ OAUTH2_SECURITY_NAME,
SecurityScheme().apply {
type = SecurityScheme.Type.OAUTH2
this.flows = flows
@@ -90,17 +91,10 @@ internal object SecuritySchemeGenerator {
}
}
- fun Operation.addSecurityItemFromSecurityRequirements(securityRequirements: SecurityRequirements?, oauth2SecuritySchemeDefinition: Oauth2Configuration?) {
+ fun Operation.addSecurityItemFromSecurityRequirements(securityRequirements: SecurityRequirements?) {
if (securityRequirements != null) {
when (securityRequirements.type) {
- SecurityType.OAUTH2 -> oauth2SecuritySchemeDefinition?.flows?.map {
- addSecurityItem(
- SecurityRequirement().addList(
- oauth2SecuritySchemeDefinition.securitySchemeName(it),
- securityRequirements2ScopesList(securityRequirements)
- )
- )
- }
+ SecurityType.OAUTH2 -> addSecurityItem(SecurityRequirement().addList(OAUTH2_SECURITY_NAME, securityRequirements2ScopesList(securityRequirements)))
SecurityType.BASIC -> addSecurityItem(SecurityRequirement().addList(BASIC_SECURITY_NAME))
SecurityType.API_KEY -> addSecurityItem(SecurityRequirement().addList(API_KEY_SECURITY_NAME))
SecurityType.JWT_BEARER -> addSecurityItem(SecurityRequirement().addList(JWT_BEARER_SECURITY_NAME))
diff --git a/restdocs-api-spec-openapi3-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3GeneratorTest.kt b/restdocs-api-spec-openapi3-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3GeneratorTest.kt
index 61eea513..97305f65 100644
--- a/restdocs-api-spec-openapi3-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3GeneratorTest.kt
+++ b/restdocs-api-spec-openapi3-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3GeneratorTest.kt
@@ -18,6 +18,7 @@ import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.Option
import io.swagger.parser.OpenAPIParser
import io.swagger.parser.models.ParseOptions
+import io.swagger.v3.oas.models.info.Contact
import io.swagger.v3.oas.models.servers.Server
import org.assertj.core.api.BDDAssertions.then
import org.junit.jupiter.api.Test
@@ -29,6 +30,17 @@ class OpenApi3GeneratorTest {
lateinit var openApiSpecJsonString: String
lateinit var openApiJsonPathContext: DocumentContext
+ @Test
+ fun `should convert multi level schema model to openapi`() {
+ givenPutProductResourceModel()
+
+ whenOpenApiObjectGenerated()
+
+ val optionDTOPath = "components.schemas.OptionDTO"
+ then(openApiJsonPathContext.read>("$optionDTOPath.properties.name")).isNotNull()
+ then(openApiJsonPathContext.read>("$optionDTOPath.properties.id")).isNotNull()
+ }
+
@Test
fun `should convert single resource model to openapi`() {
givenGetProductResourceModel()
@@ -287,6 +299,30 @@ class OpenApi3GeneratorTest {
(it["schema"] as LinkedHashMap<*, *>)["type"] == "number" &&
(it["schema"] as LinkedHashMap<*, *>)["default"] == 1
}
+ then(params).anyMatch {
+ it["name"] == "intNumberParameter" &&
+ it["description"] == "a int number parameter" &&
+ (it["schema"] as LinkedHashMap<*, *>)["type"] == "number" &&
+ (it["schema"] as LinkedHashMap<*, *>)["default"] == 1
+ }
+ then(params).anyMatch {
+ it["name"] == "longNumberParameter" &&
+ it["description"] == "a long number parameter" &&
+ (it["schema"] as LinkedHashMap<*, *>)["type"] == "number" &&
+ (it["schema"] as LinkedHashMap<*, *>)["default"] == 1
+ }
+ then(params).anyMatch {
+ it["name"] == "doubleNumberParameter" &&
+ it["description"] == "a double number parameter" &&
+ (it["schema"] as LinkedHashMap<*, *>)["type"] == "number" &&
+ (it["schema"] as LinkedHashMap<*, *>)["default"] == 1.0
+ }
+ then(params).anyMatch {
+ it["name"] == "floatNumberParameter" &&
+ it["description"] == "a float number parameter" &&
+ (it["schema"] as LinkedHashMap<*, *>)["type"] == "number" &&
+ (it["schema"] as LinkedHashMap<*, *>)["default"] == 1.0
+ }
then(params).anyMatch {
it["name"] == "integerParameter" &&
it["description"] == "a integer parameter" &&
@@ -294,6 +330,13 @@ class OpenApi3GeneratorTest {
(it["schema"] as LinkedHashMap<*, *>)["format"] == "int32" &&
(it["schema"] as LinkedHashMap<*, *>)["default"] == 2
}
+ then(params).anyMatch {
+ it["name"] == "longIntegerParameter" &&
+ it["description"] == "a long integer parameter" &&
+ (it["schema"] as LinkedHashMap<*, *>)["type"] == "integer" &&
+ (it["schema"] as LinkedHashMap<*, *>)["format"] == "int32" &&
+ (it["schema"] as LinkedHashMap<*, *>)["default"] == 2
+ }
then(params).anyMatch {
it["name"] == "X-SOME-BOOLEAN" &&
it["description"] == "a header boolean parameter" &&
@@ -312,6 +355,30 @@ class OpenApi3GeneratorTest {
(it["schema"] as LinkedHashMap<*, *>)["type"] == "number" &&
(it["schema"] as LinkedHashMap<*, *>)["default"] == 1
}
+ then(params).anyMatch {
+ it["name"] == "X-SOME-INT-NUMBER" &&
+ it["description"] == "a header int number parameter" &&
+ (it["schema"] as LinkedHashMap<*, *>)["type"] == "number" &&
+ (it["schema"] as LinkedHashMap<*, *>)["default"] == 1
+ }
+ then(params).anyMatch {
+ it["name"] == "X-SOME-LONG-NUMBER" &&
+ it["description"] == "a header long number parameter" &&
+ (it["schema"] as LinkedHashMap<*, *>)["type"] == "number" &&
+ (it["schema"] as LinkedHashMap<*, *>)["default"] == 1
+ }
+ then(params).anyMatch {
+ it["name"] == "X-SOME-DOUBLE-NUMBER" &&
+ it["description"] == "a header double number parameter" &&
+ (it["schema"] as LinkedHashMap<*, *>)["type"] == "number" &&
+ (it["schema"] as LinkedHashMap<*, *>)["default"] == 1.0
+ }
+ then(params).anyMatch {
+ it["name"] == "X-SOME-FLOAT-NUMBER" &&
+ it["description"] == "a header float number parameter" &&
+ (it["schema"] as LinkedHashMap<*, *>)["type"] == "number" &&
+ (it["schema"] as LinkedHashMap<*, *>)["default"] == 1.0
+ }
then(params).anyMatch {
it["name"] == "X-SOME-INTEGER" &&
it["description"] == "a header integer parameter" &&
@@ -319,7 +386,14 @@ class OpenApi3GeneratorTest {
(it["schema"] as LinkedHashMap<*, *>)["format"] == "int32" &&
(it["schema"] as LinkedHashMap<*, *>)["default"] == 2
}
- then(params).hasSize(9)
+ then(params).anyMatch {
+ it["name"] == "X-SOME-LONG-INTEGER" &&
+ it["description"] == "a header long integer parameter" &&
+ (it["schema"] as LinkedHashMap<*, *>)["type"] == "integer" &&
+ (it["schema"] as LinkedHashMap<*, *>)["format"] == "int32" &&
+ (it["schema"] as LinkedHashMap<*, *>)["default"] == 2
+ }
+ then(params).hasSize(19)
thenOpenApiSpecIsValid()
}
@@ -439,8 +513,7 @@ class OpenApi3GeneratorTest {
then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.content.application/json.schema.\$ref")).isNotNull()
then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.content.application/json.examples.test.value")).isNotNull()
- then(openApiJsonPathContext.read>>("$productGetByIdPath.security[*].oauth2_clientCredentials").flatMap { it }).containsOnly("prod:r")
- then(openApiJsonPathContext.read>>("$productGetByIdPath.security[*].oauth2_authorizationCode").flatMap { it }).containsOnly("prod:r")
+ then(openApiJsonPathContext.read>>("$productGetByIdPath.security[*].oauth2").flatMap { it }).containsOnly("prod:r")
}
private fun thenMultiplePathParametersExist() {
@@ -457,6 +530,7 @@ class OpenApi3GeneratorTest {
then(openApiJsonPathContext.read("info.title")).isEqualTo("API")
then(openApiJsonPathContext.read("info.description")).isEqualTo("API Description")
then(openApiJsonPathContext.read("info.version")).isEqualTo("1.0.0")
+ then(openApiJsonPathContext.read("info.contact.name")).isEqualTo("Test Contact")
}
private fun thenTagFieldsPresent() {
@@ -517,7 +591,8 @@ class OpenApi3GeneratorTest {
),
format = "json",
description = "API Description",
- tagDescriptions = mapOf("tag1" to "tag1 description", "tag2" to "tag2 description")
+ tagDescriptions = mapOf("tag1" to "tag1 description", "tag2" to "tag2 description"),
+ contact = Contact().apply { name = "Test Contact" }
)
println(openApiSpecJsonString)
@@ -864,6 +939,21 @@ class OpenApi3GeneratorTest {
)
}
+ private fun givenPutProductResourceModel() {
+ resources = listOf(
+ ResourceModel(
+ operationId = "test",
+ summary = "summary",
+ description = "description",
+ privateResource = false,
+ deprecated = false,
+ tags = setOf("tag1", "tag2"),
+ request = getProductPutRequest(),
+ response = getProductPutResponse(Schema("ProductPutResponse"))
+ )
+ )
+ }
+
private fun givenGetProductResourceModel() {
resources = listOf(
ResourceModel(
@@ -990,6 +1080,54 @@ class OpenApi3GeneratorTest {
)
}
+ private fun getProductPutResponse(schema: Schema? = null): ResponseModel {
+ return ResponseModel(
+ status = 200,
+ contentType = "application/json",
+ schema = schema,
+ headers = listOf(
+ HeaderDescriptor(
+ name = "SIGNATURE",
+ description = "This is some signature",
+ type = "STRING",
+ optional = false
+ )
+ ),
+ responseFields = listOf(
+ FieldDescriptor(
+ path = "id",
+ description = "product id",
+ type = "STRING"
+ ),
+ FieldDescriptor(
+ path = "option",
+ description = "option",
+ type = "OBJECT",
+ attributes = Attributes(schemaName = "OptionDTO")
+ ),
+ FieldDescriptor(
+ path = "option.id",
+ description = "option id",
+ type = "STRING"
+ ),
+ FieldDescriptor(
+ path = "option.name",
+ description = "option name",
+ type = "STRING"
+ ),
+ ),
+ example = """
+ {
+ "id": "pid12312",
+ "option": {
+ "id": "otid00001",
+ "name": "Option name"
+ }
+ }
+ """.trimIndent(),
+ )
+ }
+
private fun getProductHalResponse(schema: Schema? = null): ResponseModel {
return ResponseModel(
status = 200,
@@ -1085,6 +1223,51 @@ class OpenApi3GeneratorTest {
)
}
+ private fun getProductPutRequest(): RequestModel {
+ return RequestModel(
+ path = "/products/{id}",
+ method = HTTPMethod.PUT,
+ headers = listOf(),
+ pathParameters = listOf(),
+ requestParameters = listOf(),
+ securityRequirements = null,
+ requestFields = listOf(
+ FieldDescriptor(
+ path = "id",
+ description = "product id",
+ type = "STRING"
+ ),
+ FieldDescriptor(
+ path = "option",
+ description = "option",
+ type = "OBJECT",
+ attributes = Attributes(schemaName = "OptionDTO")
+ ),
+ FieldDescriptor(
+ path = "option.id",
+ description = "option id",
+ type = "STRING"
+ ),
+ FieldDescriptor(
+ path = "option.name",
+ description = "option name",
+ type = "STRING"
+ ),
+ ),
+ contentType = "application/json",
+ example = """
+ {
+ "id": "pid12312",
+ "option": {
+ "id": "otid00001",
+ "name": "Option name"
+ }
+ }
+ """.trimIndent(),
+ schema = Schema("ProductPutRequest")
+ )
+ }
+
private fun getProductRequestWithMultiplePathParameters(getSecurityRequirement: () -> SecurityRequirements = ::getOAuth2SecurityRequirement): RequestModel {
return RequestModel(
path = "/products/{id}-{subId}",
@@ -1205,12 +1388,47 @@ class OpenApi3GeneratorTest {
optional = true,
defaultValue = 1.toBigDecimal()
),
+ HeaderDescriptor(
+ name = "X-SOME-INT-NUMBER",
+ description = "a header int number parameter",
+ type = "NUMBER",
+ optional = true,
+ defaultValue = 1
+ ),
+ HeaderDescriptor(
+ name = "X-SOME-LONG-NUMBER",
+ description = "a header long number parameter",
+ type = "NUMBER",
+ optional = true,
+ defaultValue = 1L
+ ),
+ HeaderDescriptor(
+ name = "X-SOME-DOUBLE-NUMBER",
+ description = "a header double number parameter",
+ type = "NUMBER",
+ optional = true,
+ defaultValue = 1.0
+ ),
+ HeaderDescriptor(
+ name = "X-SOME-FLOAT-NUMBER",
+ description = "a header float number parameter",
+ type = "NUMBER",
+ optional = true,
+ defaultValue = 1.toFloat()
+ ),
HeaderDescriptor(
name = "X-SOME-INTEGER",
description = "a header integer parameter",
type = "INTEGER",
optional = true,
defaultValue = 2
+ ),
+ HeaderDescriptor(
+ name = "X-SOME-LONG-INTEGER",
+ description = "a header long integer parameter",
+ type = "INTEGER",
+ optional = true,
+ defaultValue = 2L
)
),
requestParameters = listOf(
@@ -1238,6 +1456,38 @@ class OpenApi3GeneratorTest {
ignored = false,
defaultValue = 1.toBigDecimal()
),
+ ParameterDescriptor(
+ name = "intNumberParameter",
+ description = "a int number parameter",
+ type = "NUMBER",
+ optional = true,
+ ignored = false,
+ defaultValue = 1
+ ),
+ ParameterDescriptor(
+ name = "longNumberParameter",
+ description = "a long number parameter",
+ type = "NUMBER",
+ optional = true,
+ ignored = false,
+ defaultValue = 1L
+ ),
+ ParameterDescriptor(
+ name = "doubleNumberParameter",
+ description = "a double number parameter",
+ type = "NUMBER",
+ optional = true,
+ ignored = false,
+ defaultValue = 1.0
+ ),
+ ParameterDescriptor(
+ name = "floatNumberParameter",
+ description = "a float number parameter",
+ type = "NUMBER",
+ optional = true,
+ ignored = false,
+ defaultValue = 1.toFloat()
+ ),
ParameterDescriptor(
name = "integerParameter",
description = "a integer parameter",
@@ -1245,6 +1495,14 @@ class OpenApi3GeneratorTest {
optional = true,
ignored = false,
defaultValue = 2
+ ),
+ ParameterDescriptor(
+ name = "longIntegerParameter",
+ description = "a long integer parameter",
+ type = "INTEGER",
+ optional = true,
+ ignored = false,
+ defaultValue = 2L
)
)
)
diff --git a/restdocs-api-spec-postman-generator/build.gradle.kts b/restdocs-api-spec-postman-generator/build.gradle.kts
index ca34a3b4..a65447b3 100644
--- a/restdocs-api-spec-postman-generator/build.gradle.kts
+++ b/restdocs-api-spec-postman-generator/build.gradle.kts
@@ -11,11 +11,11 @@ val junitVersion: String by extra
val jacksonVersion: String by extra
dependencies {
- compile(kotlin("stdlib-jdk8"))
+ implementation(kotlin("stdlib-jdk8"))
- compile(project(":restdocs-api-spec-model"))
- compile("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
- compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
+ implementation(project(":restdocs-api-spec-model"))
+ implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
testImplementation("org.assertj:assertj-core:3.10.0")
diff --git a/restdocs-api-spec-restassured/build.gradle.kts b/restdocs-api-spec-restassured/build.gradle.kts
index 2d9d02ca..757829e5 100644
--- a/restdocs-api-spec-restassured/build.gradle.kts
+++ b/restdocs-api-spec-restassured/build.gradle.kts
@@ -11,17 +11,17 @@ val springRestDocsVersion: String by extra
val junitVersion: String by extra
dependencies {
- compile(kotlin("stdlib-jdk8"))
+ implementation(kotlin("stdlib-jdk8"))
- compile(project(":restdocs-api-spec"))
- compile("org.springframework.restdocs:spring-restdocs-restassured:$springRestDocsVersion")
+ implementation(project(":restdocs-api-spec"))
+ implementation("org.springframework.restdocs:spring-restdocs-restassured:$springRestDocsVersion")
- testCompile("org.springframework.boot:spring-boot-starter-test:$springBootVersion") {
+ testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") {
exclude("junit")
}
- testCompile("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
+ testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
testImplementation("org.junit-pioneer:junit-pioneer:0.3.0")
- testCompile("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion")
+ testImplementation("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion")
}
publishing {
diff --git a/restdocs-api-spec-webtestclient/build.gradle.kts b/restdocs-api-spec-webtestclient/build.gradle.kts
index be1358f9..2a595567 100644
--- a/restdocs-api-spec-webtestclient/build.gradle.kts
+++ b/restdocs-api-spec-webtestclient/build.gradle.kts
@@ -14,18 +14,18 @@ val springRestDocsVersion: String by extra
val junitVersion: String by extra
dependencies {
- compile(kotlin("stdlib-jdk8"))
+ implementation(kotlin("stdlib-jdk8"))
- compile(project(":restdocs-api-spec"))
- compile("org.springframework.restdocs:spring-restdocs-webtestclient:$springRestDocsVersion")
+ implementation(project(":restdocs-api-spec"))
+ implementation("org.springframework.restdocs:spring-restdocs-webtestclient:$springRestDocsVersion")
- testCompile("org.springframework.boot:spring-boot-starter-test:$springBootVersion") {
+ testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") {
exclude("junit")
}
- testCompile("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
+ testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
testImplementation("org.junit-pioneer:junit-pioneer:0.3.0")
testImplementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
- testCompile("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion")
+ testImplementation("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion")
testImplementation("io.projectreactor:reactor-core:3.2.8.RELEASE")
}
diff --git a/restdocs-api-spec/build.gradle.kts b/restdocs-api-spec/build.gradle.kts
index ece57688..30fd1fde 100755
--- a/restdocs-api-spec/build.gradle.kts
+++ b/restdocs-api-spec/build.gradle.kts
@@ -14,22 +14,22 @@ val springRestDocsVersion: String by extra
val junitVersion: String by extra
dependencies {
- compile(kotlin("stdlib-jdk8"))
- compile(kotlin("reflect"))
+ implementation(kotlin("stdlib-jdk8"))
+ implementation(kotlin("reflect"))
- compile("org.springframework.restdocs:spring-restdocs-core:$springRestDocsVersion")
- compile("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
- compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
+ implementation("org.springframework.restdocs:spring-restdocs-core:$springRestDocsVersion")
+ implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
- testCompile("org.springframework.boot:spring-boot-starter-test:$springBootVersion") {
+ testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") {
exclude("junit")
}
- testCompile("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
+ testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
testImplementation("org.junit-pioneer:junit-pioneer:0.2.2")
- testCompile("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion")
- testCompile("org.hibernate.validator:hibernate-validator:6.0.10.Final")
- testCompile("org.assertj:assertj-core:3.10.0")
- testCompile("com.jayway.jsonpath:json-path:2.3.0")
+ testImplementation("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion")
+ testImplementation("org.hibernate.validator:hibernate-validator:6.0.10.Final")
+ testImplementation("org.assertj:assertj-core:3.10.0")
+ testImplementation("com.jayway.jsonpath:json-path:2.3.0")
testImplementation("com.github.java-json-tools:json-schema-validator:2.2.10")
testImplementation("com.github.erosb:everit-json-schema:1.11.0")
diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/JwtSecurityHandler.kt b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/JwtSecurityHandler.kt
index 7363db1b..2e06b9bf 100644
--- a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/JwtSecurityHandler.kt
+++ b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/JwtSecurityHandler.kt
@@ -62,9 +62,13 @@ internal class JwtSecurityHandler : SecurityRequirementsExtractor {
try {
val jwtMap = ObjectMapper().readValue