diff --git a/README.md b/README.md index 5eba7c5..501ca72 100644 --- a/README.md +++ b/README.md @@ -79,3 +79,7 @@ Interceptor для логирования запросов/ответов. ## exception-handler-logger-spring-web Добавляет логирование в обработку ошибок + +## version-spring-web + +Добавляет возможность задавать версию апи через `properties` без необходимости явно указывать в каждом маппинге diff --git a/build.gradle.kts b/build.gradle.kts index e593edb..87675fa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,6 +59,8 @@ subprojects { dependency("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") dependency("org.mockito:mockito-inline:2.13.0") + + dependency("com.github.zafarkhaja:java-semver:0.9.0") } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 3e016cf..b49f56a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,3 +30,4 @@ include("logger-spring") include("logger-spring-web") include("exception-handler-spring-web") include("exception-handler-logger-spring-web") +include("version-spring-web") diff --git a/version-spring-web/build.gradle.kts b/version-spring-web/build.gradle.kts new file mode 100644 index 0000000..a103ea4 --- /dev/null +++ b/version-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/version-spring-web/src/main/kotlin/ru/touchin/version/EnableSpringVersion.kt b/version-spring-web/src/main/kotlin/ru/touchin/version/EnableSpringVersion.kt new file mode 100644 index 0000000..6df8704 --- /dev/null +++ b/version-spring-web/src/main/kotlin/ru/touchin/version/EnableSpringVersion.kt @@ -0,0 +1,8 @@ +@file:Suppress("unused") +package ru.touchin.version + +import org.springframework.context.annotation.Import +import ru.touchin.version.configurations.SpringVersionConfiguration + +@Import(value = [SpringVersionConfiguration::class]) +annotation class EnableSpringVersion diff --git a/version-spring-web/src/main/kotlin/ru/touchin/version/annotations/Versioned.kt b/version-spring-web/src/main/kotlin/ru/touchin/version/annotations/Versioned.kt new file mode 100644 index 0000000..a138f74 --- /dev/null +++ b/version-spring-web/src/main/kotlin/ru/touchin/version/annotations/Versioned.kt @@ -0,0 +1,6 @@ +package ru.touchin.version.annotations + +@Target(allowedTargets = [AnnotationTarget.CLASS]) +internal annotation class Versioned( + val value: String = "" +) diff --git a/version-spring-web/src/main/kotlin/ru/touchin/version/annotations/VersionedApi.kt b/version-spring-web/src/main/kotlin/ru/touchin/version/annotations/VersionedApi.kt new file mode 100644 index 0000000..8fafc7e --- /dev/null +++ b/version-spring-web/src/main/kotlin/ru/touchin/version/annotations/VersionedApi.kt @@ -0,0 +1,5 @@ +package ru.touchin.version.annotations + +@Versioned("/api/v\${api.version}") +@Target(allowedTargets = [AnnotationTarget.CLASS]) +annotation class VersionedApi diff --git a/version-spring-web/src/main/kotlin/ru/touchin/version/configurations/SpringVersionConfiguration.kt b/version-spring-web/src/main/kotlin/ru/touchin/version/configurations/SpringVersionConfiguration.kt new file mode 100644 index 0000000..1bd191b --- /dev/null +++ b/version-spring-web/src/main/kotlin/ru/touchin/version/configurations/SpringVersionConfiguration.kt @@ -0,0 +1,17 @@ +package ru.touchin.version.configurations + +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import ru.touchin.version.mapping.WebMvcVersionRegistrations + +@Suppress("SpringFacetCodeInspection") +@Configuration +class SpringVersionConfiguration { + + @Bean + fun webMvcRegistrations(): WebMvcRegistrations { + return WebMvcVersionRegistrations() + } + +} diff --git a/version-spring-web/src/main/kotlin/ru/touchin/version/mapping/VersionedRequestMappingHandlerMapping.kt b/version-spring-web/src/main/kotlin/ru/touchin/version/mapping/VersionedRequestMappingHandlerMapping.kt new file mode 100644 index 0000000..dcdf71a --- /dev/null +++ b/version-spring-web/src/main/kotlin/ru/touchin/version/mapping/VersionedRequestMappingHandlerMapping.kt @@ -0,0 +1,25 @@ +package ru.touchin.version.mapping + +import org.springframework.core.annotation.AnnotatedElementUtils +import org.springframework.web.servlet.mvc.method.RequestMappingInfo +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping +import ru.touchin.version.annotations.Versioned +import java.lang.reflect.Method + +class VersionedRequestMappingHandlerMapping : RequestMappingHandlerMapping() { + + override fun getMappingForMethod(method: Method, handlerType: Class<*>): RequestMappingInfo? { + val mappingResult = super.getMappingForMethod(method, handlerType) + ?: return null + + val versionedAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerType, Versioned::class.java) + ?: return mappingResult + + val versionPath = resolveEmbeddedValuesInPatterns( + arrayOf(versionedAnnotation.value) + ) + + return RequestMappingInfo.paths(*versionPath).build().combine(mappingResult) + } + +} diff --git a/version-spring-web/src/main/kotlin/ru/touchin/version/mapping/WebMvcVersionRegistrations.kt b/version-spring-web/src/main/kotlin/ru/touchin/version/mapping/WebMvcVersionRegistrations.kt new file mode 100644 index 0000000..c04417e --- /dev/null +++ b/version-spring-web/src/main/kotlin/ru/touchin/version/mapping/WebMvcVersionRegistrations.kt @@ -0,0 +1,12 @@ +package ru.touchin.version.mapping + +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping + +class WebMvcVersionRegistrations : WebMvcRegistrations { + + override fun getRequestMappingHandlerMapping(): RequestMappingHandlerMapping { + return VersionedRequestMappingHandlerMapping() + } + +} diff --git a/version-spring-web/src/test/kotlin/ru/touchin/version/TestApplication.kt b/version-spring-web/src/test/kotlin/ru/touchin/version/TestApplication.kt new file mode 100644 index 0000000..c25afed --- /dev/null +++ b/version-spring-web/src/test/kotlin/ru/touchin/version/TestApplication.kt @@ -0,0 +1,6 @@ +package ru.touchin.version + +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication +class TestApplication diff --git a/version-spring-web/src/test/kotlin/ru/touchin/version/VersionedRequestMappingHandlerMappingMvcTest.kt b/version-spring-web/src/test/kotlin/ru/touchin/version/VersionedRequestMappingHandlerMappingMvcTest.kt new file mode 100644 index 0000000..9331b7c --- /dev/null +++ b/version-spring-web/src/test/kotlin/ru/touchin/version/VersionedRequestMappingHandlerMappingMvcTest.kt @@ -0,0 +1,75 @@ +package ru.touchin.version + +import org.hamcrest.Matchers.`is` +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.get +import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import ru.touchin.version.annotations.VersionedApi + +@VersionedApi +@RestController +@RequestMapping("/app") +class VersionController { + + @GetMapping("/version") + fun version(): Map { + return mapOf("version" to "yes") + } + +} + +@RestController +@RequestMapping("/app") +class NoVersionController { + + @GetMapping("/no-version") + fun noVersion(): Map { + return mapOf("version" to "no") + } + +} + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +internal class VersionedRequestMappingHandlerMappingMvcTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Test + @DisplayName("К пути должна добавляться версия") + fun shouldBePathWithVersion() { + mockMvc + .perform(get("/api/v6/app/version")) + .andDo(print()) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.version", `is`("yes"))) + } + + @Test + @DisplayName("К пути не должна добавляться версия") + fun shouldBePathWithoutVersion() { + mockMvc + .perform(get("/app/no-version")) + .andDo(print()) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.version", `is`("no"))) + } + +} diff --git a/version-spring-web/src/test/resources/application-test.properties b/version-spring-web/src/test/resources/application-test.properties new file mode 100644 index 0000000..771a6a9 --- /dev/null +++ b/version-spring-web/src/test/resources/application-test.properties @@ -0,0 +1,2 @@ +# suppress inspection "SpringBootApplicationProperties" +api.version=6