Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ This is why we came up with this project.
- [Build configuration](#build-configuration)
- [Gradle](#gradle)
- [Maven](#maven)
- [Usage with Spring REST Docs](#usage-with-spring-rest-docs)
- [Usage with Spring REST Docs - MockMvc](#usage-with-spring-rest-docs---mockmvc)
- [Usage with Spring REST Docs - RestAssured](#usage-with-spring-rest-docs---rest-assured)
- [Documenting Bean Validation constraints](#documenting-bean-validation-constraints)
- [Migrate existing Spring REST Docs tests](#migrate-existing-spring-rest-docs-tests)
- [Security Definitions in OpenAPI](#security-definitions-in-openapi)
Expand Down Expand Up @@ -134,7 +135,7 @@ See the [build.gradle](samples/restdocs-api-spec-sample/build.gradle) for the se
The root project does not provide a maven plugin.
But you can find a plugin that works with `restdocs-api-spec` at [BerkleyTechnologyServices/restdocs-spec](https://github.com/BerkleyTechnologyServices/restdocs-spec).

### Usage with Spring REST Docs
### Usage with Spring REST Docs - MockMvc

The class [ResourceDocumentation](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceDocumentation.kt) contains the entry point for using the [ResourceSnippet](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceSnippet.kt).

Expand Down Expand Up @@ -219,6 +220,28 @@ This makes the `urlTemplate` available in the snippet and we can depend on the n
mockMvc.perform(get("/carts/{id}", cartId)
```

### Usage with Spring REST Docs - REST Assured
The usage for REST Assured is similar to MockMVC, except that [com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt) is used instead of [com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper](restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapper.kt).

To use the ``RestAssuredRestDocumentationWrapper``, you have to add a dependency to [restdocs-api-spec-restassured](restdocs-api-spec-restassured) to your build.
```java
RestAssured.given(this.spec)
.filter(RestAssuredRestDocumentationWrapper.document("{method-name}",
"The API description",
requestParameters(
parameterWithName("param").description("the param")
),
responseFields(
fieldWithPath("doc.timestamp").description("Creation timestamp")
)
))
.when()
.queryParam("param", "foo")
.get("/restAssuredExample")
.then()
.statusCode(200);
```

### Documenting Bean Validation constraints

Similar to the way Spring REST Docs allows to use [bean validation constraints](https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#documenting-your-api-constraints) to enhance your documentation, you can also use the constraints from your model classes to let `restdocs-api-spec` enrich the generated JsonSchemas.
Expand Down Expand Up @@ -438,8 +461,3 @@ See [openapi2raml.gradle](samples/restdocs-api-spec-sample/openapi2raml.gradle).
./gradlew -b samples/restdocs-api-spec-sample/openapi2raml.gradle openapi2raml
```

## Limitations

### Rest Assured

Spring REST Docs also supports REST Assured to write tests that produce documentation. We currently have not tried REST Assured with our project.
29 changes: 29 additions & 0 deletions restdocs-api-spec-mockmvc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm")
}

repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}

val springBootVersion: String by extra
val springRestDocsVersion: String by extra
val junitVersion: String by extra

dependencies {
compile(kotlin("stdlib-jdk8"))

compile(project(":restdocs-api-spec"))
compile("org.springframework.restdocs:spring-restdocs-mockmvc:$springRestDocsVersion")

testCompile("org.springframework.boot:spring-boot-starter-test:$springBootVersion") {
exclude("junit")
}
testCompile("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")
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.epages.restdocs.apispec

import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler
import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor
import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor
import org.springframework.restdocs.snippet.Snippet
import java.util.function.Function

/**
* Convenience class to migrate to restdocs-openapi in a non-invasive way.
* It is a wrapper and replacement for MockMvcRestDocumentation that transparently adds a ResourceSnippet with the descriptors provided in the given snippets.
*/
object MockMvcRestDocumentationWrapper : RestDocumentationWrapper() {

@JvmOverloads @JvmStatic
fun document(
identifier: String,
resourceDetails: ResourceSnippetDetails,
requestPreprocessor: OperationRequestPreprocessor? = null,
responsePreprocessor: OperationResponsePreprocessor? = null,
snippetFilter: Function<List<Snippet>, List<Snippet>> = Function.identity(),
vararg snippets: Snippet
): RestDocumentationResultHandler {

val enhancedSnippets =
enhanceSnippetsWithResourceSnippet(
resourceDetails = resourceDetails,
snippetFilter = snippetFilter,
snippets = *snippets
)

if (requestPreprocessor != null && responsePreprocessor != null) {
return MockMvcRestDocumentation.document(
identifier,
requestPreprocessor,
responsePreprocessor,
*enhancedSnippets
)
} else if (requestPreprocessor != null) {
return MockMvcRestDocumentation.document(identifier, requestPreprocessor, *enhancedSnippets)
} else if (responsePreprocessor != null) {
return MockMvcRestDocumentation.document(identifier, responsePreprocessor, *enhancedSnippets)
}

return MockMvcRestDocumentation.document(identifier, *enhancedSnippets)
}

@JvmOverloads @JvmStatic
fun document(
identifier: String,
description: String? = null,
summary: String? = null,
privateResource: Boolean = false,
deprecated: Boolean = false,
requestPreprocessor: OperationRequestPreprocessor? = null,
responsePreprocessor: OperationResponsePreprocessor? = null,
snippetFilter: Function<List<Snippet>, List<Snippet>> = Function.identity(),
vararg snippets: Snippet
): RestDocumentationResultHandler {
return document(
identifier = identifier,
resourceDetails = ResourceSnippetParametersBuilder()
.description(description)
.summary(summary)
.privateResource(privateResource)
.deprecated(deprecated),
requestPreprocessor = requestPreprocessor,
responsePreprocessor = responsePreprocessor,
snippetFilter = snippetFilter,
snippets = *snippets
)
}

@JvmStatic
fun document(
identifier: String,
requestPreprocessor: OperationRequestPreprocessor,
vararg snippets: Snippet
): RestDocumentationResultHandler {
return document(identifier, null, null, false, false, requestPreprocessor, snippets = *snippets)
}

@JvmStatic
fun document(
identifier: String,
description: String,
privateResource: Boolean,
vararg snippets: Snippet
): RestDocumentationResultHandler {
return document(identifier, description, null, privateResource, snippets = *snippets)
}

@JvmStatic
fun resourceDetails(): ResourceSnippetDetails {
return ResourceSnippetParametersBuilder()
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.epages.restdocs.apispec

import com.epages.restdocs.apispec.ResourceDocumentation.resource
import org.assertj.core.api.Assertions.assertThatCode
import org.assertj.core.api.BDDAssertions.then
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.hateoas.MediaTypes.HAL_JSON
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.restdocs.headers.HeaderDocumentation.headerWithName
import org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders
import org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders
Expand All @@ -20,10 +24,14 @@ import org.springframework.restdocs.request.RequestDocumentation.pathParameters
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.io.File

@ExtendWith(SpringExtension::class)
@WebMvcTest
class MockMvcRestDocumentationWrapperIntegrationTest(@Autowired mockMvc: MockMvc) : ResourceSnippetIntegrationTest(mockMvc) {
class MockMvcRestDocumentationWrapperIntegrationTest(@Autowired private val mockMvc: MockMvc) : ResourceSnippetIntegrationTest() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would rather not share code between the tests. Mainly because I think the gradle setup for this is a little exotic and also complex. I would rather go for the code duplication here.


@Test
fun should_document_both_restdocs_and_resource() {
Expand Down Expand Up @@ -68,6 +76,81 @@ class MockMvcRestDocumentationWrapperIntegrationTest(@Autowired mockMvc: MockMvc
thenSnippetFileExists()
}

@Test
fun should_document_request() {
givenEndpointInvoked()

whenResourceSnippetDocumentedWithoutParameters()

thenSnippetFileExists()
}

@Test
fun should_document_request_with_description() {
givenEndpointInvoked()

whenResourceSnippetDocumentedWithDescription()

thenSnippetFileExists()
}

@Test
fun should_document_request_with_fields() {
givenEndpointInvoked()

whenResourceSnippetDocumentedWithRequestAndResponseFields()

thenSnippetFileExists()
}

@Test
fun should_document_request_with_null_field() {
givenEndpointInvoked("null")

assertThatCode { this.whenResourceSnippetDocumentedWithRequestAndResponseFields() }
.doesNotThrowAnyException()
}

private fun whenResourceSnippetDocumentedWithoutParameters() {
resultActions
.andDo(document(operationName, resource()))
}

private fun whenResourceSnippetDocumentedWithDescription() {
resultActions
.andDo(document(operationName, resource("A description")))
}

private fun whenResourceSnippetDocumentedWithRequestAndResponseFields() {
resultActions
.andDo(document(operationName, buildFullResourceSnippet()))
}

private fun givenEndpointInvoked(flagValue: String = "true") {
resultActions = mockMvc.perform(
post("/some/{someId}/other/{otherId}", "id", 1)
.contentType(APPLICATION_JSON)
.header("X-Custom-Header", "test")
.accept(HAL_JSON)
.content("""{
"comment": "some",
"flag": $flagValue,
"count": 1
}""".trimIndent()
)
).andExpect(status().isOk)
}

private fun thenSnippetFileExists() {
with(generatedSnippetFile()) {
then(this).exists()
val contents = readText()
then(contents).isNotEmpty()
}
}

private fun generatedSnippetFile() = File("build/generated-snippets", "$operationName/resource.json")

@Throws(Exception::class)
private fun whenDocumentedWithRestdocsAndResource() {
resultActions
Expand Down
Loading