From 9c5d10aa0770c47a77aaa119f05f8567d8358b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kulcs=C3=A1r=20Roland?= Date: Sun, 27 May 2018 13:40:12 +0200 Subject: [PATCH] #715 - Add link and affordance DSLs to help write idiomatic Kotlin code. * Provide a LinkBuilderDsl for help create links. * Provider an AffordanceBuilderDsl to help create affordances. --- pom.xml | 65 +++++++++-- .../hateoas/AffordanceBuilderDsl.kt | 59 ++++++++++ .../springframework/hateoas/LinkBuilderDsl.kt | 87 +++++++++++++++ .../hateoas/AffordanceBuilderDslUnitTest.kt | 94 ++++++++++++++++ .../hateoas/LinkBuilderDslUnitTest.kt | 102 ++++++++++++++++++ 5 files changed, 400 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/org/springframework/hateoas/AffordanceBuilderDsl.kt create mode 100644 src/main/kotlin/org/springframework/hateoas/LinkBuilderDsl.kt create mode 100644 src/test/kotlin/org/springframework/hateoas/AffordanceBuilderDslUnitTest.kt create mode 100644 src/test/kotlin/org/springframework/hateoas/LinkBuilderDslUnitTest.kt diff --git a/pom.xml b/pom.xml index 9242adcf..a15b4f46 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,7 @@ 1.7.25 5.0.11.RELEASE 2.0.0.BUILD-SNAPSHOT + 1.2.71 @@ -422,6 +423,26 @@ + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + true + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + true + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + + org.springframework spring-aop @@ -554,13 +575,6 @@ test - - xmlunit - xmlunit - 1.6 - test - - net.jadler jadler-all @@ -663,6 +677,43 @@ false + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/main/java + + + + + test-compile + + test-compile + + + + ${project.basedir}/src/test/kotlin + ${project.basedir}/src/test/java + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + org.apache.maven.plugins diff --git a/src/main/kotlin/org/springframework/hateoas/AffordanceBuilderDsl.kt b/src/main/kotlin/org/springframework/hateoas/AffordanceBuilderDsl.kt new file mode 100644 index 00000000..8fd9e62c --- /dev/null +++ b/src/main/kotlin/org/springframework/hateoas/AffordanceBuilderDsl.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.hateoas + +import org.springframework.hateoas.mvc.ControllerLinkBuilder +import org.springframework.hateoas.mvc.ControllerLinkBuilder.afford +import org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn + +/** + * Create a new [Link] with additional [Affordance]s. + * + * @author Roland Kulcsár + * @since 1.0 + */ +inline infix fun Link.andAffordances(setup: AffordanceBuilderDsl.() -> Unit): Link { + + val builder = AffordanceBuilderDsl() + builder.setup() + + return andAffordances(builder.affordances) +} + +/** + * Extract a [Link] from the [ControllerLinkBuilder] and look up the related [Affordance]. Should + * only be one. + * + * @author Roland Kulcsár + * @since 1.0 + */ +inline fun afford(func: C.() -> Unit): Affordance = afford(methodOn(C::class.java).apply(func)) + +/** + * Provide an [Affordance]s DSL to help write idiomatic Kotlin code. + * + * @author Roland Kulcsár + * @since 1.0 + */ +open class AffordanceBuilderDsl(val affordances: MutableList = mutableListOf()) { + + inline fun afford(func: C.() -> Any) { + + val affordance = afford(methodOn(C::class.java).func()) + affordances.add(affordance) + } +} diff --git a/src/main/kotlin/org/springframework/hateoas/LinkBuilderDsl.kt b/src/main/kotlin/org/springframework/hateoas/LinkBuilderDsl.kt new file mode 100644 index 00000000..426a1d45 --- /dev/null +++ b/src/main/kotlin/org/springframework/hateoas/LinkBuilderDsl.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.hateoas + +import org.springframework.hateoas.mvc.ControllerLinkBuilder +import org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo +import org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn +import kotlin.reflect.KClass + +/** + * Create a [ControllerLinkBuilder] pointing to a [func] method. + * + * @author Roland Kulcsár + * @since 1.0 + */ +inline fun linkTo(func: C.() -> Unit): ControllerLinkBuilder = linkTo(methodOn(C::class.java).apply(func)) + +/** + * Create a [Link] with the given [rel]. + * + * @author Roland Kulcsár + * @since 1.0 + */ +infix fun ControllerLinkBuilder.withRel(rel: String): Link = withRel(rel) + +/** + * Add [links] to the [R] resource. + * + * @author Roland Kulcsár + * @since 1.0 + */ +fun R.add(controller: Class, links: LinkBuilderDsl.(R) -> Unit): R { + + val builder = LinkBuilderDsl(controller, this) + builder.links(this) + + return this +} + +/** + * Add [links] to the [R] resource. + * + * @author Roland Kulcsár + * @since 1.0 + */ +fun R.add(controller: KClass, links: LinkBuilderDsl.(R) -> Unit): R { + return add(controller.java, links) +} + +/** + * Provide a [LinkBuilder] DSL to help write idiomatic Kotlin code. + * + * @author Roland Kulcsár + * @since 1.0 + */ +open class LinkBuilderDsl(val controller: Class, val resource: R) { + + /** + * Create a [ControllerLinkBuilder] pointing to [func] method. + */ + fun linkTo(func: C.() -> R): ControllerLinkBuilder = linkTo(methodOn(controller).run(func)) + + /** + * Add a link with the given [rel] to the [resource]. + */ + infix fun ControllerLinkBuilder.withRel(rel: String): Link { + + val link = withRel(rel) + resource.add(link) + + return link + } +} diff --git a/src/test/kotlin/org/springframework/hateoas/AffordanceBuilderDslUnitTest.kt b/src/test/kotlin/org/springframework/hateoas/AffordanceBuilderDslUnitTest.kt new file mode 100644 index 00000000..4aec1aa7 --- /dev/null +++ b/src/test/kotlin/org/springframework/hateoas/AffordanceBuilderDslUnitTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.hateoas + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.springframework.http.HttpMethod +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +/** + * Unit tests for [LinkBuilderDsl] and [AffordanceBuilderDsl]. + * + * @author Roland Kulcsár + */ +class AffordanceBuilderDslUnitTest : TestUtils() { + + /** + * @see #715 + */ + @Test + fun `creates affordance to controller method`() { + + val delete = afford { delete("15") } + + assertThat(delete.httpMethod).isEqualTo(HttpMethod.DELETE) + assertThat(delete.name).isEqualTo("delete") + } + + /** + * @see #715 + */ + @Test + fun `creates link to controller method with affordances`() { + + val id = "15" + val self = linkTo { findById(id) } withRel Link.REL_SELF + val selfWithAffordances = self andAffordances { + afford { update(id, CustomerDTO("John Doe")) } + afford { delete(id) } + } + + assertThat(selfWithAffordances.rel).isEqualTo(Link.REL_SELF) + assertThat(selfWithAffordances.href).isEqualTo("http://localhost/customers/15") + + assertThat(selfWithAffordances.affordances).hasSize(3) + assertThat(selfWithAffordances.affordances[0].httpMethod).isEqualTo(HttpMethod.GET) + assertThat(selfWithAffordances.affordances[0].name).isEqualTo("findById") + + assertThat(selfWithAffordances.affordances[1].httpMethod).isEqualTo(HttpMethod.PUT) + assertThat(selfWithAffordances.affordances[1].name).isEqualTo("update") + + assertThat(selfWithAffordances.affordances[2].httpMethod).isEqualTo(HttpMethod.DELETE) + assertThat(selfWithAffordances.affordances[2].name).isEqualTo("delete") + + assertThat(selfWithAffordances.hashCode()).isNotEqualTo(self.hashCode()) + assertThat(selfWithAffordances).isNotEqualTo(self) + } + + data class Customer(val id: String, val name: String) + data class CustomerDTO(val name: String) + open class CustomerResource(val id: String, val name: String) : ResourceSupport() + open class ProductResource(val id: String) : ResourceSupport() + + @RequestMapping("/customers") + interface CustomerController { + + @GetMapping("/{id}") + fun findById(@PathVariable id: String): ResponseEntity + + @GetMapping("/{id}/products") + fun findProductsById(@PathVariable id: String): PagedResources + + @PutMapping("/{id}") + fun update(@PathVariable id: String, @RequestBody customer: CustomerDTO): ResponseEntity + + @DeleteMapping("/{id}") + fun delete(@PathVariable id: String): ResponseEntity + } +} diff --git a/src/test/kotlin/org/springframework/hateoas/LinkBuilderDslUnitTest.kt b/src/test/kotlin/org/springframework/hateoas/LinkBuilderDslUnitTest.kt new file mode 100644 index 00000000..55198af0 --- /dev/null +++ b/src/test/kotlin/org/springframework/hateoas/LinkBuilderDslUnitTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.hateoas + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +/** + * Unit tests for [LinkBuilderDsl] and [AffordanceBuilderDsl]. + * + * @author Roland Kulcsár + */ +class LinkBuilderDslUnitTest : TestUtils() { + + private val REL_PRODUCTS = "products" + + /** + * @see #715 + */ + @Test + fun `creates link to controller method`() { + + val self = linkTo { findById("15") } withRel Link.REL_SELF + + assertPointsToMockServer(self) + assertThat(self.rel).isEqualTo(Link.REL_SELF) + assertThat(self.href).endsWith("/customers/15") + } + + /** + * @see #715 + */ + @Test + fun `adds links to wrapped domain object`() { + + val customer = Resource(Customer("15", "John Doe")) + + customer.add(CustomerController::class) { + linkTo { findById(it.content.id) } withRel Link.REL_SELF + linkTo { findProductsById(it.content.id) } withRel REL_PRODUCTS + } + + customer.links.forEach { assertPointsToMockServer(it) } + assertThat(customer.hasLink(Link.REL_SELF)).isTrue() + assertThat(customer.hasLink(REL_PRODUCTS)).isTrue() + } + + /** + * @see #715 + */ + @Test + fun `adds links to resourcesupport object`() { + + val customer = CustomerResource("15", "John Doe") + + customer.add(CustomerController::class) { + linkTo { findById(it.id) } withRel Link.REL_SELF + linkTo { findProductsById(it.id) } withRel REL_PRODUCTS + } + + customer.links.forEach { assertPointsToMockServer(it) } + assertThat(customer.hasLink(Link.REL_SELF)).isTrue() + assertThat(customer.hasLink(REL_PRODUCTS)).isTrue() + } + + data class Customer(val id: String, val name: String) + data class CustomerDTO(val name: String) + open class CustomerResource(val id: String, val name: String) : ResourceSupport() + open class ProductResource(val id: String) : ResourceSupport() + + @RequestMapping("/customers") + interface CustomerController { + + @GetMapping("/{id}") + fun findById(@PathVariable id: String): ResponseEntity + + @GetMapping("/{id}/products") + fun findProductsById(@PathVariable id: String): PagedResources + + @PutMapping("/{id}") + fun update(@PathVariable id: String, @RequestBody customer: CustomerDTO): ResponseEntity + + @DeleteMapping("/{id}") + fun delete(@PathVariable id: String): ResponseEntity + } +}