Skip to content

Commit 32f2058

Browse files
committed
Add link builder DSL to write idiomatic Kotlin code
1 parent 26c9a3d commit 32f2058

File tree

3 files changed

+272
-0
lines changed

3 files changed

+272
-0
lines changed

pom.xml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,26 @@
422422

423423
<dependencies>
424424

425+
<!-- Kotlin extension -->
426+
<dependency>
427+
<groupId>org.jetbrains.kotlin</groupId>
428+
<artifactId>kotlin-stdlib</artifactId>
429+
<version>1.2.71</version>
430+
<optional>true</optional>
431+
</dependency>
432+
<dependency>
433+
<groupId>org.jetbrains.kotlin</groupId>
434+
<artifactId>kotlin-reflect</artifactId>
435+
<version>1.2.71</version>
436+
<optional>true</optional>
437+
</dependency>
438+
<dependency>
439+
<groupId>org.jetbrains.kotlin</groupId>
440+
<artifactId>kotlin-test</artifactId>
441+
<version>1.2.71</version>
442+
<scope>test</scope>
443+
</dependency>
444+
425445
<dependency>
426446
<groupId>org.springframework</groupId>
427447
<artifactId>spring-aop</artifactId>
@@ -663,6 +683,26 @@
663683
<useSystemClassLoader>false</useSystemClassLoader>
664684
</configuration>
665685
</plugin>
686+
<plugin>
687+
<groupId>org.jetbrains.kotlin</groupId>
688+
<artifactId>kotlin-maven-plugin</artifactId>
689+
<version>1.2.71</version>
690+
<executions>
691+
<execution>
692+
<id>compile</id>
693+
<phase>process-sources</phase>
694+
<goals>
695+
<goal>compile</goal>
696+
</goals>
697+
</execution>
698+
</executions>
699+
</plugin>
700+
701+
<plugin>
702+
<groupId>org.apache.maven.plugins</groupId>
703+
<artifactId>maven-deploy-plugin</artifactId>
704+
<version>2.8.2</version>
705+
</plugin>
666706

667707
<plugin>
668708
<groupId>org.apache.maven.plugins</groupId>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.hateoas
18+
19+
import org.springframework.hateoas.mvc.ControllerLinkBuilder
20+
import org.springframework.hateoas.mvc.ControllerLinkBuilder.afford
21+
import org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo
22+
import org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn
23+
import kotlin.reflect.KClass
24+
25+
/**
26+
* Creates the [Link] with the given [rel].
27+
*
28+
* @author Roland Kulcsár
29+
*/
30+
infix fun ControllerLinkBuilder.withRel(rel: String): Link = withRel(rel)
31+
32+
/**
33+
* Create new [Link] with additional [Affordance]s.
34+
*
35+
* @author Roland Kulcsár
36+
*/
37+
inline infix fun Link.andAffordances(setup: AffordancesBuilderDsl.() -> Unit): Link {
38+
val builder = AffordancesBuilderDsl()
39+
builder.setup()
40+
41+
return andAffordances(builder.affordances)
42+
}
43+
44+
/**
45+
* Creates a [ControllerLinkBuilder] pointing to [func] method.
46+
*
47+
* @author Roland Kulcsár
48+
*/
49+
inline fun <reified C> linkTo(func: C.() -> Unit): ControllerLinkBuilder = linkTo(methodOn(C::class.java).apply(func))
50+
51+
/**
52+
* Extract a [Link] from the [ControllerLinkBuilder] and look up the related [Affordance]. Should
53+
* only be one.
54+
*
55+
* @author Roland Kulcsár
56+
*/
57+
inline fun <reified C> afford(func: C.() -> Unit): Affordance = afford(methodOn(C::class.java).apply(func))
58+
59+
/**
60+
* Adds the [links] to the [R] resource.
61+
*
62+
* @author Roland Kulcsár
63+
*/
64+
fun <C, R : ResourceSupport> R.add(controller: Class<C>, links: LinkBuilderDsl<C, R>.(R) -> Unit): R {
65+
val builder = LinkBuilderDsl(controller, this)
66+
builder.links(this)
67+
68+
return this
69+
}
70+
71+
/**
72+
* Adds the [links] to the [R] resource.
73+
*
74+
* @author Roland Kulcsár
75+
*/
76+
fun <C : Any, R : ResourceSupport> R.add(controller: KClass<C>, links: LinkBuilderDsl<C, R>.(R) -> Unit): R {
77+
return add(controller.java, links)
78+
}
79+
80+
/**
81+
* Provides a link builder Kotlin DSL in order to be able to write idiomatic Kotlin code.
82+
*
83+
* @author Roland Kulcsár
84+
*/
85+
open class LinkBuilderDsl<C, R : ResourceSupport>(val controller: Class<C>, val resource: R) {
86+
87+
/**
88+
* Creates a [ControllerLinkBuilder] pointing to [func] method.
89+
*/
90+
fun <R> linkTo(func: C.() -> R): ControllerLinkBuilder = linkTo(methodOn(controller).run(func))
91+
92+
/**
93+
* Adds link with the given [rel] to [resource].
94+
*/
95+
infix fun ControllerLinkBuilder.withRel(rel: String): Link {
96+
val link = withRel(rel)
97+
resource.add(link)
98+
99+
return link
100+
}
101+
}
102+
103+
/**
104+
* Provides an affordances builder Kotlin DSL in order to be able to write idiomatic Kotlin code.
105+
*
106+
* @author Roland Kulcsár
107+
*/
108+
open class AffordancesBuilderDsl(val affordances: MutableList<Affordance> = mutableListOf()) {
109+
110+
inline fun <reified C> afford(func: C.() -> Any) {
111+
val affordance = afford(methodOn(C::class.java).func())
112+
affordances.add(affordance)
113+
}
114+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.hateoas
18+
19+
import org.assertj.core.api.Assertions.assertThat
20+
import org.junit.Test
21+
import org.springframework.http.HttpMethod
22+
import org.springframework.http.ResponseEntity
23+
import org.springframework.web.bind.annotation.DeleteMapping
24+
import org.springframework.web.bind.annotation.GetMapping
25+
import org.springframework.web.bind.annotation.PathVariable
26+
import org.springframework.web.bind.annotation.PutMapping
27+
import org.springframework.web.bind.annotation.RequestBody
28+
import org.springframework.web.bind.annotation.RequestMapping
29+
30+
/**
31+
* Unit tests for [LinkBuilderDsl].
32+
*
33+
* @author Roland Kulcsár
34+
*/
35+
class LinkBuilderDslUnitTest : TestUtils() {
36+
37+
private val REL_PRODUCTS = "products"
38+
39+
@Test
40+
fun `creates link to controller method`() {
41+
val self = linkTo<CustomerController> { findById("15") } withRel Link.REL_SELF
42+
43+
assertPointsToMockServer(self)
44+
assertThat(self.rel).isEqualTo(Link.REL_SELF)
45+
assertThat(self.href).endsWith("/customers/15")
46+
}
47+
48+
@Test
49+
fun `creates affordance to controller method`() {
50+
val delete = afford<CustomerController> { delete("15") }
51+
52+
assertThat(delete.httpMethod).isEqualTo(HttpMethod.DELETE)
53+
assertThat(delete.name).isEqualTo("delete")
54+
}
55+
56+
@Test
57+
fun `creates link to controller method with affordances`() {
58+
val id = "15"
59+
val self = linkTo<CustomerController> { findById(id) } withRel Link.REL_SELF
60+
val selfWithAffordances = self andAffordances {
61+
afford<CustomerController> { update(id, CustomerDto("John Doe")) }
62+
afford<CustomerController> { delete(id) }
63+
}
64+
65+
assertThat(selfWithAffordances.affordances).hasSize(3)
66+
assertThat(selfWithAffordances.hashCode()).isNotEqualTo(self.hashCode())
67+
assertThat(selfWithAffordances).isNotEqualTo(self)
68+
}
69+
70+
@Test
71+
fun `adds links to wrapped domain object`() {
72+
val customer = Resource(Customer("15", "John Doe"))
73+
74+
customer.add(CustomerController::class) {
75+
linkTo { findById(it.content.id) } withRel Link.REL_SELF
76+
linkTo { findProductsById(it.content.id) } withRel REL_PRODUCTS
77+
}
78+
79+
customer.links.forEach { assertPointsToMockServer(it) }
80+
assertThat(customer.hasLink(Link.REL_SELF)).isTrue()
81+
assertThat(customer.hasLink(REL_PRODUCTS)).isTrue()
82+
}
83+
84+
@Test
85+
fun `adds links to resource`() {
86+
val customer = CustomerResource("15", "John Doe")
87+
88+
customer.add(CustomerController::class) {
89+
linkTo { findById(it.id) } withRel Link.REL_SELF
90+
linkTo { findProductsById(it.id) } withRel REL_PRODUCTS
91+
}
92+
93+
customer.links.forEach { assertPointsToMockServer(it) }
94+
assertThat(customer.hasLink(Link.REL_SELF)).isTrue()
95+
assertThat(customer.hasLink(REL_PRODUCTS)).isTrue()
96+
}
97+
98+
data class Customer(val id: String, val name: String)
99+
data class CustomerDto(val name: String)
100+
open class CustomerResource(val id: String, val name: String) : ResourceSupport()
101+
open class ProductResource(val id: String) : ResourceSupport()
102+
103+
@RequestMapping("/customers")
104+
interface CustomerController {
105+
106+
@GetMapping("/{id}")
107+
fun findById(@PathVariable id: String): ResponseEntity<CustomerResource>
108+
109+
@GetMapping("/{id}/products")
110+
fun findProductsById(@PathVariable id: String): PagedResources<ProductResource>
111+
112+
@PutMapping("/{id}")
113+
fun update(@PathVariable id: String, @RequestBody customer: CustomerDto): ResponseEntity<CustomerResource>
114+
115+
@DeleteMapping("/{id}")
116+
fun delete(@PathVariable id: String): ResponseEntity<Unit>
117+
}
118+
}

0 commit comments

Comments
 (0)