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
2 changes: 1 addition & 1 deletion .github/workflows/deploy-to-mavencentral.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
MAVEN_CENTRAL_PASSWORD:
required: true
jobs:
publish-to-maven-central:
publish-to-mavencentral:
runs-on: ubuntu-latest
steps:
- name: GitHub 리포지토리 체크아웃
Expand Down
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# 📝 Documentify
[![Kotlin](https://img.shields.io/badge/kotlin-2.0.0-blue.svg?logo=kotlin)](http://kotlinlang.org)
![Latest Release](https://img.shields.io/github/v/release/BGMSound/documentify)
[![Apache 2.0 license](https://img.shields.io/badge/License-APACHE%202.0-green.svg?logo=APACHE&style=flat)](https://opensource.org/licenses/Apache-2.0)
<br>
Expand Down Expand Up @@ -35,7 +36,7 @@ fun setUp(provider: RestDocumentationContextProvider) {
```
You can also set up the test environment with an application context or an auto-configured MockMvc (or WebTestClient).
<br><br>
`Mvc Example`
`MVC Example`
```kotlin
webApplicationContext(provider, context)
mockMvc(provider, mockMvc)
Expand Down Expand Up @@ -77,11 +78,29 @@ fun documentationGetApi() {
}
```

Additional validation of the mock response generated during the tests for document creation is also possible.
```kotlin
@Test
fun documentationGetApi() {
documentation("test-get-api") {
information {
summary("test get api")
description("this is test get api")
tag("test")
}
requestLine(Method.GET, "/api/test/{path}")
responseBody {
field("testField", "test", "test")
}
}.expect(jsonPath("$testField").value("test"))
}
```

### Generate OpenAPI Specification
After setting up the test environment and writing the test code, run the test.
The OpenAPI specification document will be generated in the `build/generated-snippets` directory.

First, apply documentify plugin to your `build.gradle.kts` file:
First, apply documentify plugin to your `build.gradle.kts` file *(need gradle plugin portal)*:
```kotlin
plugins {
id("io.github.bgmsound.documentify") version "${version}"
Expand Down Expand Up @@ -112,3 +131,6 @@ you can also create Postman collection by running the following command:

## Documentify Development Story
If you want to check out the development story of Documentify, please refer to the [blog post](https://bgmsound.medium.com/documentify-선언형-rest-docs-dsl-제작기-0a09f651be2c).

## License
documentify is Open Source software released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html).
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ version = extra["project.version.id"] as String

val publicModulePathSet = setOf(
rootProject.projects.documentifyProject.documentifyCore.identityPath.path,
rootProject.projects.documentifyProject.documentifyPlugin.identityPath.path,
rootProject.projects.documentifyProject.documentifyGradlePlugin.identityPath.path,
rootProject.projects.documentifyProject.documentifyMvc.identityPath.path,
rootProject.projects.documentifyProject.documentifyReactive.identityPath.path,
rootProject.projects.documentifyStarters.documentifyStarterMvc.identityPath.path,
Expand Down Expand Up @@ -67,7 +67,7 @@ subprojects {
configure<MavenPublishBaseExtension> {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)

val artifactId = name.replace("-plugin", ".gradle.plugin")
val artifactId = name.replace("-gradle-plugin", ".gradle.plugin")
val projectGroup = property("project.group").toString()
val projectName = property("project.name").toString()
val projectVersion = property("project.version.id").toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import org.springframework.restdocs.snippet.Snippet

abstract class AbstractDocumentEmitter(
protected val provider: RestDocumentationContextProvider,
protected val documentSpec: DocumentSpec
) {
protected val documentSpec: DocumentSpec,
protected val sampleAggregator: DocumentSpecSampleAggregator = DefaultDocumentSpecSampleAggregator
) : DocumentEmitter {
protected fun ResponseSpec.buildResource(index: Int): Snippet {
val resourceBuilder = ResourceSnippetParameters.builder()
if (documentSpec.tags.isNotEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.github.bgmsound.documentify.core.emitter

import io.github.bgmsound.documentify.core.specification.element.field.Field
import io.github.bgmsound.documentify.core.specification.schema.response.ResponseSpec

abstract class AbstractDocumentResult : DocumentResult {
abstract fun validateJsonPath(jsonResultMatcher: JsonResultMatcher)

override fun validateWith(responseSpec: ResponseSpec) {
val matchers = aggregateMatchers(responseSpec.fields)
if (matchers.isEmpty()) {
return
}
matchers.forEach { matcher ->
validateJsonPath(matcher)
}
}

private fun aggregateMatchers(fields: List<Field>): List<JsonResultMatcher> {
return fields.filter {
it.hasSample() || it.canHaveChild() || !it.isIgnored()
}.flatMap {
it.aggregateMatchers()
}
}

private fun Field.aggregateMatchers(): List<JsonResultMatcher> {
if (isIgnored()) {
return emptyList()
}
if (!hasSample() && !canHaveChild()) {
return emptyList()
}
val matchers = mutableListOf<JsonResultMatcher>()
if (hasSample()) {
val jsonPath = StringBuilder("$.${path}").apply {
if (isArray()) {
append("[*]")
}
}.toString().replace("[]", "[*]")
matchers.add(JsonResultMatcher.of(jsonPath, sample))
} else if (childFields().isNotEmpty()) {
val childMatchers = aggregateMatchers(childFields())
matchers.addAll(childMatchers)
}
return matchers
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@ package io.github.bgmsound.documentify.core.emitter
import io.github.bgmsound.documentify.core.specification.element.SpecElement
import io.github.bgmsound.documentify.core.specification.element.field.Field

object SpecElementSampleAssociater {
fun List<Field>.associatedFieldSample(): Map<String, Any> {
return filter {
object DefaultDocumentSpecSampleAggregator : DocumentSpecSampleAggregator {
@Suppress("UNCHECKED_CAST")
override fun <T : SpecElement> aggregate(specElements: List<T>): Map<String, Any> {
if (specElements.isEmpty()) {
return emptyMap()
}
if (!specElements.isField()) {
return specElements.associate {
it.key to it.sample
}
}
val fieldElements = specElements as List<Field>
return fieldElements.filter {
it.hasSample() || it.canHaveChild() || !it.isIgnored()
}.associate {
it.associatedSample()
it.aggregateSample()
}
}

fun Field.associatedSample(): Pair<String, Any> {
private fun Field.aggregateSample(): Pair<String, Any> {
if (isIgnored()) {
throw IllegalStateException("can't associate ignored field $key")
}
Expand All @@ -25,7 +35,7 @@ object SpecElementSampleAssociater {
if (childFields().isEmpty()) {
throw IllegalStateException("Field $key must have child fields")
}
val sample = childFields().associate { it.associatedSample() }
val sample = childFields().associate { it.aggregateSample() }
if (isArray()) {
listOf(sample)
} else {
Expand All @@ -34,9 +44,9 @@ object SpecElementSampleAssociater {
}
}

fun List<SpecElement>.associatedSample(): Map<String, Any> {
return associate {
it.key to it.sample
private fun List<SpecElement>.isField(): Boolean {
return all {
it is Field
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.bgmsound.documentify.core.emitter

interface DocumentEmitter
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.bgmsound.documentify.core.emitter

import io.github.bgmsound.documentify.core.specification.schema.response.ResponseSpec

interface DocumentResult {

fun validateWith(responseSpec: ResponseSpec)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.bgmsound.documentify.core.emitter

import io.github.bgmsound.documentify.core.specification.element.SpecElement

interface DocumentSpecSampleAggregator{

fun <T : SpecElement> aggregate(specElements: List<T>) : Map<String, Any>

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.github.bgmsound.documentify.core.emitter

data class JsonResultMatcher(
val jsonPath: String,
val expectedValue: Any
) {
companion object {
fun of(path: String, expectedValue: Any) = JsonResultMatcher(path, expectedValue)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import com.fasterxml.jackson.databind.ObjectMapper
abstract class AbstractStandaloneContextEnvironment<T : StandaloneContextEnvironment<T>> : StandaloneContextEnvironment<T>, AbstractDocumentContextEnvironment() {
protected val controllers: MutableList<Any> = mutableListOf()
protected val controllerAdvices: MutableList<Any> = mutableListOf()
protected var objectMapper: ObjectMapper? = null
protected var codec: ObjectMapper? = null

override fun objectMapper(objectMapper: ObjectMapper): T {
this.objectMapper = objectMapper
override fun codec(codec: ObjectMapper): T {
this.codec = codec
return this as T
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper

interface StandaloneContextEnvironment<T : StandaloneContextEnvironment<T>> : DocumentContextEnvironment {

fun objectMapper(objectMapper: ObjectMapper): T
fun codec(codec: ObjectMapper): T

fun controller(controller: Any): T

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ gradlePlugin {
plugins {
register("documentify") {
id = "io.github.bgmsound.documentify"
implementationClass = "io.github.bgmsound.documentify.plugin.DocumentifyPlugin"
implementationClass = "io.github.bgmsound.documentify.gradle.plugin.DocumentifyPlugin"
displayName = "Documentify"
description = "easy and powerful API documentation tool for spring restdocs"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.github.bgmsound.documentify.plugin
package io.github.bgmsound.documentify.gradle.plugin

import org.gradle.api.Plugin
import org.gradle.api.Project
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.github.bgmsound.documentify.gradle.plugin.extensions

interface DocumentifyExtension {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.github.bgmsound.documentify.mvc

import io.github.bgmsound.documentify.core.specification.schema.document.DocumentSpec
import io.github.bgmsound.documentify.mvc.emitter.EmitterFactory
import io.github.bgmsound.documentify.mvc.emitter.MvcDocumentEmitter
import io.github.bgmsound.documentify.mvc.environment.MockMvcContextEnvironment.Companion.mockMvcEnvironment
import io.github.bgmsound.documentify.mvc.environment.StandaloneMvcContextEnvironment
import io.github.bgmsound.documentify.mvc.environment.WebApplicationContextEnvironment.Companion.webApplicationContextEnvironment
Expand All @@ -15,32 +16,39 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver
@ExtendWith(RestDocumentationExtension::class)
abstract class Documentify {
private lateinit var provider: RestDocumentationContextProvider
private lateinit var documentContextEnvironment: MvcDocumentContextEnvironment
private lateinit var environment: MvcDocumentContextEnvironment
private var customEmitter: MvcDocumentEmitter? = null

fun documentation(
name: String,
specCustomizer: DocumentSpec.() -> Unit
): ValidatableMockResponse {
val documentSpec = DocumentSpec(name).also { specCustomizer(it) }
val emitter = EmitterFactory.createMvcEmitter(provider, documentSpec, documentContextEnvironment)
val emitter = customEmitter ?: EmitterFactory.of(provider, documentSpec, environment)

return emitter.emit()
}

fun emitter(
customEmitter: MvcDocumentEmitter
) {
this.customEmitter = customEmitter
}

fun mockMvc(
provider: RestDocumentationContextProvider,
mockMvc: MockMvc
) {
this.provider = provider
documentContextEnvironment = mockMvcEnvironment(mockMvc)
environment = mockMvcEnvironment(mockMvc)
}

fun standalone(
provider: RestDocumentationContextProvider,
standaloneContext: StandaloneMvcContextEnvironment
) {
this.provider = provider
documentContextEnvironment = standaloneContext
environment = standaloneContext
}

fun standalone(
Expand Down Expand Up @@ -71,7 +79,7 @@ abstract class Documentify {
provider: RestDocumentationContextProvider,
context: WebApplicationContext
) {
documentContextEnvironment = webApplicationContextEnvironment(provider, context)
environment = webApplicationContextEnvironment(provider, context)
this.provider = provider
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ class ValidatableMockMvcResponseAdapter private constructor(
return this
}

fun status(status: HttpStatus): ValidatableMockResponse {
override fun status(status: HttpStatus): ValidatableMockResponse {
restAssuredMockResponse.statusCode(status.value())
return this
}

fun apply(handler: ResultHandler, vararg additionalHandlers: ResultHandler): ValidatableMockResponse {
override fun apply(handler: ResultHandler, vararg additionalHandlers: ResultHandler): ValidatableMockResponse {
restAssuredMockResponse.apply(handler, *additionalHandlers)
return this
}

fun assertThat(matcher: ResultMatcher): ValidatableMockResponse {
override fun assertThat(matcher: ResultMatcher): ValidatableMockResponse {
restAssuredMockResponse.assertThat(matcher)
return this
}
Expand Down
Loading
Loading