Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,4 @@ abstract class AbstractDocumentifySupport : DocumentifySupport {
environment = applicationContextEnvironment(provider, context)
this.provider = provider
}

companion object
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ 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
import org.hamcrest.BaseMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import java.math.BigDecimal

abstract class AbstractDocumentResult : DocumentResult {
abstract fun expect(jsonPath: String, matcher: Matcher<*>)

abstract fun expectValue(jsonPath: String, value: Any)

override fun validateWith(responseSpec: ResponseSpec) {
val matchers = aggregateMatchers(responseSpec.fields)
if (matchers.isEmpty()) {
Expand All @@ -18,15 +19,47 @@ abstract class AbstractDocumentResult : DocumentResult {
matchers.forEach { matcher ->
val jsonPath = matcher.jsonPath
val value = matcher.expectedValue
if (jsonPath.endsWith("[*]")) {
if (value !is List<*>) {
throw IllegalArgumentException("sample value type must be List")
when {
jsonPath.endsWith("[*]") -> {
if (value !is List<*>) {
throw IllegalArgumentException("sample value type must be List")
}
expect(
jsonPath.substringBeforeLast("[*]"),
Matchers.containsInAnyOrder(value.map { equalityMatcher(it) })
)
}
jsonPath.contains("[*]") -> {
expect(jsonPath, Matchers.hasItem(equalityMatcher(value)))
}
expect(jsonPath.substringBeforeLast("[*]"), Matchers.containsInAnyOrder(*value.toTypedArray()))
} else if (jsonPath.contains("[*]") && !jsonPath.endsWith("[*]")) {
expect(jsonPath, Matchers.hasItem(value))
} else {
expectValue(jsonPath, value)
else -> {
expect(jsonPath, equalityMatcher(value))
}
}
}
}

private fun equalityMatcher(expected: Any?): Matcher<Any?> {
if (expected is Number) {
val expectedDecimal = BigDecimal(expected.toString())
return object : BaseMatcher<Any?>() {
override fun matches(actual: Any?): Boolean {
val number = actual as? Number ?: return false
return runCatching {
BigDecimal(number.toString()).compareTo(expectedDecimal) == 0
}.getOrDefault(false)
}

override fun describeTo(description: Description) {
description.appendText("a number numerically equal to ").appendValue(expected)
}
}
}
return object : BaseMatcher<Any?>() {
override fun matches(actual: Any?): Boolean = actual == expected

override fun describeTo(description: Description) {
description.appendValue(expected)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ class WebTestClientDocumentEmitter(
.method(method())
.uri(uriTemplate, samplePathVariables + queryVariables)
.headers { headers ->
headers.addAll(sampleHeaders.toMultiValueMap())
sampleHeaders.forEach { (key, value) ->
headers.add(key, value.toString())
}
}
.bodyIfExist(sampleFields)
.exchange()
Expand Down Expand Up @@ -75,7 +77,7 @@ class WebTestClientDocumentEmitter(
.configureClient()
.filter(WebTestClientRestDocumentation.documentationConfiguration(provider))
.build()

val snippets = response.build()
webTestClient
.method(method())
.uri(uriTemplate, samplePathVariables + queryVariables)
Expand All @@ -91,7 +93,7 @@ class WebTestClientDocumentEmitter(
"${documentSpec.name}-case-${index + 1}",
preprocessRequest(prettyPrint(), *requestPreprocessors),
preprocessResponse(prettyPrint(), *responsePreprocessors),
response.buildResource(index)
*(snippets + listOf(response.buildResource(index))).toTypedArray()
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package io.github.bgmsound.documentify.core.emitter

import io.github.bgmsound.documentify.core.specification.schema.response.ResponseSpec
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.springframework.test.web.reactive.server.WebTestClient

class WebTestClientDocumentResult(
Expand All @@ -12,10 +11,6 @@ class WebTestClientDocumentResult(
actualResponse.jsonPath(jsonPath).value(matcher)
}

override fun expectValue(jsonPath: String, value: Any) {
actualResponse.jsonPath(jsonPath).value(Matchers.equalToObject(value))
}

companion object {
fun WebTestClient.BodyContentSpec.validateWith(responseSpec: ResponseSpec) {
WebTestClientDocumentResult(this).validateWith(responseSpec)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ interface FieldSchema {

fun optionalField(path: String, description: String): Field

fun optionalField(description: String) {
optionalField("", description)
fun optionalField(description: String): Field {
return optionalField("", description)
}

fun optionalField(path: String, description: String, sample: Any): Field
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
package io.github.bgmsound.documentify.core.specification.element

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import org.springframework.restdocs.snippet.AbstractDescriptor
import java.time.LocalDate
import java.time.LocalDateTime

abstract class SpecElement(
private val descriptor: AbstractDescriptor<*>
) {
private val objectMapper = ObjectMapper()

abstract val key: String
val description get() = descriptor.description as String
val sample: Any get() {
return if (descriptor.sample() is LocalDate || descriptor.sample() is LocalDateTime) {
objectMapper.writeValueAsString(descriptor.sample())
val raw = descriptor.sample()
return if (raw is LocalDate || raw is LocalDateTime) {
OBJECT_MAPPER.writeValueAsString(raw).removeSurrounding("\"")
} else {
descriptor.sample()
raw
}
}

Expand All @@ -30,6 +30,10 @@ abstract class SpecElement(

companion object {
const val SAMPLE_KEY = "sample"

private val OBJECT_MAPPER: ObjectMapper = ObjectMapper()
.findAndRegisterModules()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
}

enum class Requirement {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class Header(
companion object {
fun newHeader(
key: String,
sample: String,
description: String,
sample: String,
requirement: Requirement
): Header {
val descriptor = HeaderDocumentation.headerWithName(key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ class ResourceSpec(

init {
summary = ""
val randomSuffix = randomSuffix()
request.schema = "$documentName Request ($randomSuffix)"
response.schema = "$documentName Response ($randomSuffix)"
request.schema = "$documentName Request"
response.schema = "$documentName Response"
}
Comment on lines 24 to 28

fun link(rel: String): Link {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.github.bgmsound.documentify.core

import io.github.bgmsound.documentify.core.emitter.AbstractDocumentResult
import io.github.bgmsound.documentify.core.specification.schema.response.ResponseSpec
import org.assertj.core.api.Assertions.assertThat
import org.hamcrest.Matcher
import org.junit.jupiter.api.Test
import java.math.BigDecimal

class DocumentResultTest {

private class CapturingDocumentResult : AbstractDocumentResult() {
val captured = mutableListOf<Pair<String, Matcher<*>>>()
override fun expect(jsonPath: String, matcher: Matcher<*>) {
captured += jsonPath to matcher
}
}

private fun matcherFor(spec: ResponseSpec.() -> Unit): Matcher<*> {
val response = ResponseSpec().apply(spec)
val result = CapturingDocumentResult()
result.validateWith(response)
return result.captured.single().second
}

@Test
fun `numeric matcher treats integer long and big decimal as equal`() {
val matcher = matcherFor { body { field("amount", "amount", 1L) } }

assertThat(matcher.matches(1)).isTrue()
assertThat(matcher.matches(1L)).isTrue()
assertThat(matcher.matches(BigDecimal.ONE)).isTrue()
assertThat(matcher.matches(1.0)).isTrue()
assertThat(matcher.matches(2)).isFalse()
}

@Test
fun `non numeric value uses strict equality`() {
val matcher = matcherFor { body { field("name", "name", "John") } }

assertThat(matcher.matches("John")).isTrue()
assertThat(matcher.matches("Jane")).isFalse()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@ class FieldTest {
assertThat(color.build().single().type).isEqualTo(JsonFieldType.STRING)
}

@Test
fun `local date sample serializes to iso string without quotes`() {
val field = ResponseBodySpec().field("date", "date", LocalDate.of(2024, 1, 1))

assertThat(field.sample).isEqualTo("2024-01-01")
}

@Test
fun `local date time sample serializes to iso string without quotes`() {
val field = ResponseBodySpec().field("dateTime", "dateTime", LocalDateTime.of(2024, 1, 1, 12, 30, 45))

assertThat(field.sample).isEqualTo("2024-01-01T12:30:45")
}

@Test
fun `adding a child to a scalar field throws`() {
val name = ResponseBodySpec().field("name", "name", "John")
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
project.name=documentify

project.group=io.github.bgmsound
project.version.id=1.4.0
project.version.id=1.4.1
project.artifact=documentify

project.description=Documentify is a tool to generate API documentation from source code.
Expand Down
Loading