diff --git a/README.md b/README.md index 501ca72..f017337 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,7 @@ Interceptor для логирования запросов/ответов. ## version-spring-web Добавляет возможность задавать версию апи через `properties` без необходимости явно указывать в каждом маппинге + +## response-wrapper-spring-web + +Добавляет обертку для успешного ответа diff --git a/response-wrapper-spring-web/build.gradle.kts b/response-wrapper-spring-web/build.gradle.kts new file mode 100644 index 0000000..a103ea4 --- /dev/null +++ b/response-wrapper-spring-web/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("kotlin") + id("kotlin-spring") + id("maven-publish") +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + implementation("org.springframework.boot:spring-boot-starter-web") + + testImplementation("org.springframework.boot:spring-boot-starter-test") +} diff --git a/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/EnableSpringResponseWrapper.kt b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/EnableSpringResponseWrapper.kt new file mode 100644 index 0000000..53dab03 --- /dev/null +++ b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/EnableSpringResponseWrapper.kt @@ -0,0 +1,8 @@ +@file:Suppress("unused") +package ru.touchin.wrapper + +import org.springframework.context.annotation.Import +import ru.touchin.wrapper.configurations.SpringResponseWrapper + +@Import(value = [SpringResponseWrapper::class]) +annotation class EnableSpringResponseWrapper diff --git a/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/advices/WrapResponseAdvice.kt b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/advices/WrapResponseAdvice.kt new file mode 100644 index 0000000..84b03ff --- /dev/null +++ b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/advices/WrapResponseAdvice.kt @@ -0,0 +1,39 @@ +@file:Suppress("unused") +package ru.touchin.wrapper.advices + +import org.springframework.core.MethodParameter +import org.springframework.http.MediaType +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.http.server.ServerHttpRequest +import org.springframework.http.server.ServerHttpResponse +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice +import ru.touchin.wrapper.annotations.NoResponseWrap +import ru.touchin.wrapper.annotations.ResponseWrap +import ru.touchin.wrapper.components.ResponseBodyWrapper + +@RestControllerAdvice(annotations = [ResponseWrap::class]) +class WrapResponseAdvice( + private val responseWrapper: ResponseBodyWrapper +): ResponseBodyAdvice { + + override fun supports(returnType: MethodParameter, converterType: Class>): Boolean { + return !returnType.hasMethodAnnotation(NoResponseWrap::class.java) + } + + /*** + * Не будет работать, если контроллер возвращает тип String, так как по умолчанию будет выбираться конвертер для строки. + * Решить проблему можно так: https://stackoverflow.com/questions/44121648/controlleradvice-responsebodyadvice-failed-to-enclose-a-string-response + */ + override fun beforeBodyWrite( + body: Any?, + returnType: MethodParameter, + selectedContentType: MediaType, + selectedConverterType: Class>, + request: ServerHttpRequest, + response: ServerHttpResponse + ): Any { + return responseWrapper.wrap(body) + } + +} diff --git a/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/annotations/NoResponseWrap.kt b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/annotations/NoResponseWrap.kt new file mode 100644 index 0000000..5a84585 --- /dev/null +++ b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/annotations/NoResponseWrap.kt @@ -0,0 +1,4 @@ +package ru.touchin.wrapper.annotations + +@Target(allowedTargets = [AnnotationTarget.FUNCTION, AnnotationTarget.CLASS]) +annotation class NoResponseWrap diff --git a/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/annotations/ResponseWrap.kt b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/annotations/ResponseWrap.kt new file mode 100644 index 0000000..8a87f3e --- /dev/null +++ b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/annotations/ResponseWrap.kt @@ -0,0 +1,4 @@ +package ru.touchin.wrapper.annotations + +@Target(allowedTargets = [AnnotationTarget.FUNCTION, AnnotationTarget.CLASS]) +annotation class ResponseWrap diff --git a/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/components/ResponseBodyWrapper.kt b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/components/ResponseBodyWrapper.kt new file mode 100644 index 0000000..9fe5ce4 --- /dev/null +++ b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/components/ResponseBodyWrapper.kt @@ -0,0 +1,7 @@ +package ru.touchin.wrapper.components + +interface ResponseBodyWrapper { + + fun wrap(body: Any?): Any + +} diff --git a/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/components/ResponseBodyWrapperImpl.kt b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/components/ResponseBodyWrapperImpl.kt new file mode 100644 index 0000000..620cbda --- /dev/null +++ b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/components/ResponseBodyWrapperImpl.kt @@ -0,0 +1,22 @@ +@file:Suppress("unused") +package ru.touchin.wrapper.components + +import org.springframework.stereotype.Component +import ru.touchin.wrapper.dto.DefaultWrapper +import ru.touchin.wrapper.dto.Wrapper + +@Component +class ResponseBodyWrapperImpl : ResponseBodyWrapper { + + override fun wrap(body: Any?): Any { + if (body is Wrapper) { + return body + } + + return DefaultWrapper( + result = body, + errorCode = 0 + ) + } + +} diff --git a/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/configurations/SpringResponseWrapper.kt b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/configurations/SpringResponseWrapper.kt new file mode 100644 index 0000000..b449724 --- /dev/null +++ b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/configurations/SpringResponseWrapper.kt @@ -0,0 +1,9 @@ +package ru.touchin.wrapper.configurations + +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration + +@Suppress("SpringFacetCodeInspection") +@Configuration +@ComponentScan("ru.touchin.wrapper.advices", "ru.touchin.wrapper.components") +class SpringResponseWrapper diff --git a/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/dto/DefaultWrapper.kt b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/dto/DefaultWrapper.kt new file mode 100644 index 0000000..d21693e --- /dev/null +++ b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/dto/DefaultWrapper.kt @@ -0,0 +1,8 @@ +package ru.touchin.wrapper.dto + +@Suppress("unused") +class DefaultWrapper( + override val result: Any?, + val errorCode: Int, + val errorMessage: String? = null +): Wrapper diff --git a/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/dto/Wrapper.kt b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/dto/Wrapper.kt new file mode 100644 index 0000000..44dbaa1 --- /dev/null +++ b/response-wrapper-spring-web/src/main/kotlin/ru/touchin/wrapper/dto/Wrapper.kt @@ -0,0 +1,5 @@ +package ru.touchin.wrapper.dto + +interface Wrapper { + val result: Any? +} diff --git a/response-wrapper-spring-web/src/test/kotlin/ru/touchin/web/TestApplication.kt b/response-wrapper-spring-web/src/test/kotlin/ru/touchin/web/TestApplication.kt new file mode 100644 index 0000000..0a78938 --- /dev/null +++ b/response-wrapper-spring-web/src/test/kotlin/ru/touchin/web/TestApplication.kt @@ -0,0 +1,8 @@ +package ru.touchin.web + +import org.springframework.boot.autoconfigure.SpringBootApplication +import ru.touchin.wrapper.EnableSpringResponseWrapper + +@SpringBootApplication +@EnableSpringResponseWrapper +class TestApplication diff --git a/response-wrapper-spring-web/src/test/kotlin/ru/touchin/web/wrapper/WrapResponseAdviceMvcTest.kt b/response-wrapper-spring-web/src/test/kotlin/ru/touchin/web/wrapper/WrapResponseAdviceMvcTest.kt new file mode 100644 index 0000000..9469b5a --- /dev/null +++ b/response-wrapper-spring-web/src/test/kotlin/ru/touchin/web/wrapper/WrapResponseAdviceMvcTest.kt @@ -0,0 +1,70 @@ +package ru.touchin.web.wrapper + +import org.hamcrest.Matchers +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultHandlers +import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import ru.touchin.wrapper.annotations.NoResponseWrap +import ru.touchin.wrapper.annotations.ResponseWrap + +@RestController +@ResponseWrap +@RequestMapping("/wrapper") +class WrapperController { + + @GetMapping("/wrap") + fun wrap(): Map { + return mapOf("wrap" to "yes") + } + + @NoResponseWrap + @GetMapping("/no-wrap") + fun noWrap(): Map { + return mapOf("wrap" to "no") + } + +} + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +internal class WrapResponseAdviceMvcTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Test + @DisplayName("Результат должен быть обернут") + fun shouldBeWrappedResponse() { + mockMvc + .perform(MockMvcRequestBuilders.get("/wrapper/wrap")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.result.wrap", Matchers.`is`("yes"))) + .andExpect(MockMvcResultMatchers.jsonPath("$.errorCode", Matchers.`is`(0))) + } + + @Test + @DisplayName("Результат НЕ должен быть обернут") + fun shouldBeNoWrappedResponse() { + mockMvc + .perform(MockMvcRequestBuilders.get("/wrapper/no-wrap")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.wrap", Matchers.`is`("no"))) + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b49f56a..e2e5cd0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,3 +31,4 @@ include("logger-spring-web") include("exception-handler-spring-web") include("exception-handler-logger-spring-web") include("version-spring-web") +include("response-wrapper-spring-web")