Skip to content

Provide link builder DSL to write idiomatic Kotlin code #715

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

rolandKulcsar
Copy link
Contributor

@rolandKulcsar rolandKulcsar commented May 27, 2018

Hi Spring and Kotlin fans,

I want to share with you my link builder extensions to be able to write idiomatic Kotlin code with Spring HATEOAS.

Updated at Jan 1 2019

We have the following resources

open class CustomerResource(val id: String) : ResourceSupport()
open class ProductResource(val id: String) : ResourceSupport()

and controller

@RequestMapping("/customers")
interface CustomerController {

    @GetMapping("/{id}")
    fun findById(@PathVariable id: String): CustomerResource

    @GetMapping("/{id}/products")
    fun findProductsById(@PathVariable id: String): List<ProductResource>

    @PutMapping("/{id}")
    fun update(@PathVariable id: String, @RequestBody customer: CustomerDto): ResponseEntity<CustomerResource>

    @DeleteMapping("/{id}")
    fun delete(@PathVariable id: String): ResponseEntity<Unit>
}

Example 1
With link builder extensions to create a self link you can simply write that

val self = linkTo<CustomerController> { findById("15") } withRel "self"

Java equivalent

Link self = linkTo(methodOn(CustomerController.class).findById("15")).withRel("self");

Kotlin equivalent without extensions

val self = linkTo(methodOn(CustomerController::class.java).findById("15")).withRel("self")

Example 2
With link builder extensions to create and add self, products links to CustomerResource you can simply write that

val customer = CustomerResource("15")
customer.add(CustomerController::class) {
    linkTo { findById(it.id) } withRel "self"
    linkTo { findProductsById(it.id) } withRel "products"
}

Java equivalent

CustomerResource customer = new CustomerResource("15");
customer.add(linkTo(methodOn(CustomerController.class).findById("15")).withRel("self"));
customer.add(linkTo(methodOn(CustomerController.class).findProductsById("15")).withRel("products"));

Kotlin equivalent without extensions

val customer = CustomerResource("15")
customer.add(linkTo(methodOn(CustomerController::class.java).findById("15")).withRel("self"))
customer.add(linkTo(methodOn(CustomerController::class.java).findProductsById("15")).withRel("products"))

Example 3
To create and add self, products links to a wrapped domain object you can simply write that

data class Customer(val id: String)

val customer = Resource(Customer("15"))
customer.add(CustomerController::class) {
    linkTo { findById(it.content.id) } withRel "self"
    linkTo { findProductsById(it.content.id) } withRel "products"
}

Example 4
Creates affordance to a controller method:

val delete = afford<CustomerController> { delete("15") }

Example 5

Creates link with affordances:

val selfWithAffordances = linkTo<CustomerController> { findById(id) } withRel "self" andAffordances {
    afford<CustomerController> { update(id, CustomerDto("John Doe")) }
    afford<CustomerController> { delete(id) }
}

Restrictions:
You have to make open your resource classes (or use the kotlin allopen plugin) because of cglib proxy issues.

@gregturn
Copy link
Contributor

Looks most exciting. Tell me, could you also add test cases for the new Affordances API? I would like to know we have solid support for that as well.

@rolandKulcsar
Copy link
Contributor Author

@gregturn Sure!
And I try to write some extensions for the Affordances API too.

@gregturn
Copy link
Contributor

I'm wondering if we shouldn't turn on into methodOn so it's purpose is consistent with that existing method.

@gregturn
Copy link
Contributor

    @Test
    fun `adds links to wrapped domain object`() {

        val customer = Resource(Customer("15"))

        customer.add(CustomerController::class) {
            methodOn { findById(it.content.id) } withRel Link.REL_SELF
            methodOn { findProductsById(it.content.id) } withRel "products"
        }

        customer.links.forEach { assertPointsToMockServer(it) }
        assertThat(customer.getLink("self")).isNotEmpty
        assertThat(customer.getLink("products")).isNotEmpty
    }

@rolandKulcsar
Copy link
Contributor Author

@gregturn It's OK for me, thanks for the suggestion!
I will rename when I finish the Affordance API test cases.

@rolandKulcsar
Copy link
Contributor Author

rolandKulcsar commented May 31, 2018

@gregturn What do you think about these?

val delete = afford<CustomerController> { delete("15") }

and

val self = linkTo<CustomerController> { 
    methodOn { findById("15") }
    andAffordances {
        afford<CustomerController> { delete("15") }
        afford<RoleController> { update("15", "ADMIN") }
    }
} withRel Link.REL_SELF

@gregturn
Copy link
Contributor

gregturn commented Jun 6, 2018

@sdeleuze I would certainly appreciate your inputs on this endeavor. While familiar with Kotlin, my knowledge is probably over a year old and not detailed.

@sdeleuze
Copy link

What about adding these 2 extensions: methodOn<CustomerController>() and .linkTo(...) designed for usage with named parameters in order to be able to write: val self = methodOn<CustomerController>().findById("15").linkTo(rel = "self")?

@rolandKulcsar
Copy link
Contributor Author

@sdeleuze You're right, I think it is a good idea to add these extensions because they are very similar to the original usage.
@sdeleuze @gregturn So, is it OK if I implement these 2 extensions and keep the rest DSL?

@sdeleuze
Copy link

@rolandKulcsar Could you please detail what other parts of the DSL you would keep in addition to these 2 ones ?

@rolandKulcsar
Copy link
Contributor Author

@sdeleuze I was thinking of those things that are in my #715 (comment) and #715 (comment) comment.

@gregturn
Copy link
Contributor

I should have done this sooner, but linked is an example Spring MVC controller that has multiple methods combined with usage of the Affordances API.

https://github.com/spring-projects/spring-hateoas-examples/blob/master/affordances/src/main/java/org/springframework/hateoas/examples/EmployeeController.java

I would seek a Kotlin-based variant that presumably is much slicker.

@gregturn
Copy link
Contributor

@rolandKulcsar If you are able to add operations for building affordances, then that might get us in a position to merge your PR.

@rolandKulcsar rolandKulcsar force-pushed the LinkBuilderKotlinDsl branch 2 times, most recently from bacba91 to 1d967bb Compare January 1, 2019 13:23
@rolandKulcsar
Copy link
Contributor Author

rolandKulcsar commented Jan 1, 2019

Happy New Year to everyone! 🥂

I added support to the Affordances API.

Example 4
Creates affordance to a controller method:

val delete = afford<CustomerController> { delete("15") }

Example 5
Creates link with affordances:

val selfWithAffordances = linkTo<CustomerController> { findById(id) } withRel "self" andAffordances {
    afford<CustomerController> { update(id, CustomerDto("John Doe")) }
    afford<CustomerController> { delete(id) }
}

@gregturn
Copy link
Contributor

gregturn commented Jan 1, 2019

Woot @rolandKulcsar!

This is a good start for a New Year. I am eager to look into this tomorrow when I resume working.

@gregturn
Copy link
Contributor

gregturn commented Jan 1, 2019

My attempt to read this PR is tricky. If you could rebase against master, it might slim it down to just your code.

@rolandKulcsar
Copy link
Contributor Author

I rebased against master and squashed the commits.

gregturn pushed a commit that referenced this pull request Jan 2, 2019
* Provide a LinkBuilderDsl for help create links.
* Provider an AffordanceBuilderDsl to help create affordances.
@gregturn gregturn self-assigned this Jan 2, 2019
@gregturn gregturn added this to the 1.0 M1 milestone Jan 2, 2019
@gregturn gregturn closed this Jan 2, 2019
@gregturn
Copy link
Contributor

gregturn commented Jan 2, 2019

Resolved via 9c5d10a.

Thanks again @rolandKulcsar!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants