Skip to content

Commit

Permalink
readme, javadocs and fix for SpringControllerLinkBuilder (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
yelouarti authored Dec 22, 2024
1 parent c0fba74 commit 611178e
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 10 deletions.
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Building hypermedia-driven APIs in reactive Spring applications using WebFlux ca

- **Resource Wrappers:** `HalResourceWrapper` and `HalListWrapper` to encapsulate resources and collections.
- **Type-Safe Link Building:** Easily create and manage hypermedia links.
- **Specialized Response Types:** Purpose-built reactive response handling with `HalResourceResponse`, `HalMultiResourceResponse`, and `HalListResponse`.
- **Pagination Support:** Simplified pagination with metadata and navigation links.
- **URI Template Support:** Define dynamic URLs with placeholders.
- **Seamless Spring Integration:** Works effortlessly with existing Spring configurations and annotations.
Expand Down Expand Up @@ -101,7 +102,7 @@ dependencies {
```
## Basic Usage
### Creating a HalResourceWrapper
Here's a simple example of how to create a `HalResourceWrapper` for an OrderDTO without any embedded resources.
Here's a simple example of how to create a `HalResourceWrapper` for an `OrderDTO` without any embedded resources.
```java
@GetMapping("/order-no-embedded/{orderId}")
public Mono<HalResourceWrapper<OrderDTO, Void>> getOrder(@PathVariable int orderId) {
Expand Down Expand Up @@ -133,6 +134,35 @@ public Mono<HalResourceWrapper<OrderDTO, Void>> getOrder(@PathVariable int order
}
}
```
### Response Types
hateoflux provides specialized response types (basically reactive `ResponseEntity`s) to handle different resource scenarios in reactive applications. The following controller method is from the previous example now altered however altered to return a reactive HTTP response, while preserving the same body:

hateoflux provides specialized response types (essentially reactive `ResponseEntity`s) to handle different resource scenarios in reactive applications. Here's the previous controller example modified to return a reactive HTTP response while preserving the same body:

```java
@GetMapping("/order-no-embedded/{orderId}")
public HalResourceResponse<OrderDTO, Void> getOrder(@PathVariable String orderId) {

Mono<HalResourceWrapper<OrderDTO, Void>> order = orderService.getOrder(orderId)
.map(order -> HalResourceWrapper.wrap(order)
.withLinks(
Link.of("orders/{orderId}/shipment")
.expand(orderId)
.withRel("shipment"),
Link.linkAsSelfOf("orders/" + orderId)
));

return HalResourceResponse.ok(order)
.withContentType(MediaType.APPLICATION_JSON)
.withHeader("Custom-Header", "value");
}
```
The library provides three response types for different scenarios:

* `HalResourceResponse`: For single HAL resources (shown above)
* `HalMultiResourceResponse`: For streaming multiple resources individually
* `HalListResponse`: For collections as a single HAL document, including pagination

## Advanced Usage
### Assemblers
Assemblers in hateoflux reduce boilerplate by handling the wrapping and linking logic. Implement either `FlatHalWrapperAssembler` for resources without embedded entities or `EmbeddingHalWrapperAssembler` for resources with embedded entities.
Expand Down Expand Up @@ -186,23 +216,25 @@ Link userLink = linkTo(UserController.class, controller -> controller.getUser("1
### Demos
Explore practical examples and debug them in the [hateoflux-demos](https://github.com/kamillionlabs/hateoflux-demos) repository. Fork the repository and run the applications to see hateoflux in action.
### Cookbook
Refer to the [Cookbook: Examples & Use Cases](https://hateoflux.kamillionlabs.de/docs/cookbook.html) for detailed and explained scenarios and code snippets demonstrating various functionalities of hateoflux.
Refer to the [Cookbook: Examples & Use Cases](https://hateoflux.kamillionlabs.de/cookbook/cookbook.html) for detailed and explained scenarios and code snippets demonstrating various functionalities of hateoflux.

## Documentation
Comprehensive documentation is available at [https://hateoflux.kamillionlabs.de (english)](https://hateoflux.kamillionlabs.de), covering:
- [What is hateoflux?](https://hateoflux.kamillionlabs.de/)
- [Representation Model](https://hateoflux.kamillionlabs.de/docs/core-concepts/representation-model.html)
- [Response Types](https://hateoflux.kamillionlabs.de/docs/core-concepts/response-handling.html)
- [Link Building](https://hateoflux.kamillionlabs.de/docs/core-concepts/linkbuilding.html)
- [Assemblers](https://hateoflux.kamillionlabs.de/docs/core-concepts/assemblers.html)
- [Spring HATEOAS vs. hateoflux](https://hateoflux.kamillionlabs.de/docs/spring-vs-hateoflux.html)
- [Cookbook: Examples & Use Cases](https://hateoflux.kamillionlabs.de/docs/cookbook.html)
- [Cookbook: Examples & Use Cases](https://hateoflux.kamillionlabs.de/docs/cookbook/)

## Comparison with Spring HATEOAS
hateoflux is specifically designed for reactive Spring WebFlux applications, offering a more streamlined and maintainable approach compared to Spring HATEOAS in reactive environments. Key differences include:

| **Aspect** | **Spring HATEOAS (WebFlux)** | **hateoflux** |
|--------------------------------|--------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
| **Representation Models** | Uses wrappers and inheritance-based models, requiring manual embedding of resources via inheritance or separate classes. | Uses wrappers exclusively to keep domain models clean and decoupled. |
| **Response Types** | Uses standard `ResponseEntity` with manual reactive flow handling | Dedicated response types optimized for different resource scenarios |
| **Assemblers and Boilerplate** | Verbose with manual resource wrapping and link addition. | Simplified with built-in methods; only links need to be specified in assemblers. |
| **Pagination Handling** | Limited support in reactive environments; requires manual implementation. | Easy pagination with HalListWrapper; handles metadata and navigation links automatically. |
| **Documentation Support** | Better for Spring MVC; less comprehensive for WebFlux. | Tailored for reactive Spring WebFlux with focused documentation and examples. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,24 @@
*/
public class HttpHeadersModule<HttpHeadersModuleT extends HttpHeadersModule<HttpHeadersModuleT>> {

/**
* The underlying {@link HttpHeaders} instance being built.
*
* <p>This field holds the HTTP headers that are being constructed using the builder methods.
*/
protected HttpHeaders headers;


/**
* Constructs a new {@code HttpHeadersModule} instance with an empty {@link HttpHeaders}.
*
* <p>This default constructor initializes the builder with an empty set of HTTP headers,
* ready to be populated using the provided builder methods.</p>
* s/
* public HttpHeadersModule() {
* this.headers = new HttpHeaders();
* }
* <p>
* /**
* Adds a new header with the specified name and values.
*
* <p>If the header already exists, the new values are appended to the existing ones.</p>
Expand Down Expand Up @@ -131,25 +145,61 @@ public HttpHeadersModuleT withETag(@NonNull String eTag) {
return (HttpHeadersModuleT) this;
}


/**
* Sets the {@code Content-Type} header to the specified media type.
*
* @param mediaType
* the media type string to set; must not be {@code null} or empty
* @throws IllegalArgumentException
* if {@code mediaType} is {@code null} or empty
*/
protected void addContentType(@NonNull String mediaType) {
Assert.notNull(mediaType, valueNotAllowedToBeNull("MediaType"));
Assert.isTrue(!mediaType.isBlank(), valueNotAllowedToBeEmpty("MediaType"));
putNewHeader(HttpHeaders.CONTENT_TYPE, mediaType);
}

/**
* Sets the {@code Location} header to the specified location URI.
*
* @param location
* the location URI string to set; must not be {@code null} or empty
* @throws IllegalArgumentException
* if {@code location} is {@code null} or empty
*/
protected void addLocation(@NonNull String location) {
Assert.notNull(location, valueNotAllowedToBeNull("Location URI"));
Assert.isTrue(!location.isBlank(), valueNotAllowedToBeEmpty("Location URI"));
putNewHeader(HttpHeaders.LOCATION, location);
}

/**
* Sets the {@code ETag} header to the specified ETag value.
*
* @param eTag
* the ETag value to set; must not be {@code null} or empty
* @throws IllegalArgumentException
* if {@code eTag} is {@code null} or empty
*/
protected void addETag(@NonNull String eTag) {
Assert.notNull(eTag, valueNotAllowedToBeNull("ETag"));
Assert.isTrue(!eTag.isBlank(), valueNotAllowedToBeEmpty("ETag"));
putNewHeader(HttpHeaders.ETAG, eTag);
}

/**
* Inserts a new header into the {@link HttpHeaders} instance.
*
* <p>If headers already exist, they are preserved and the new header is added alongside them.
* If the header with the specified key already exists, the provided values are appended.</p>
*
* @param key
* the name of the header to add; must not be {@code null} or empty
* @param values
* the values of the header to add; must not be {@code null}
* @throws IllegalArgumentException
* if {@code key} is {@code null} or empty
*/
protected void putNewHeader(String key, String... values) {
Assert.notNull(key, valueNotAllowedToBeNull("Key of attribute to put in header"));
Assert.isTrue(!key.isBlank(), valueNotAllowedToBeNull("Key of attribute to put in header"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
*/
public interface ReactiveResponseEntity {

/**
* The default HTTP status to be used when no specific status is provided. This constant is set to
* {@link HttpStatus#OK} (200), indicating a successful request.
*/
HttpStatus DEFAULT_STATUS = HttpStatus.OK;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler;

Expand All @@ -17,6 +18,21 @@
@ImportAutoConfiguration(WebFluxAutoConfiguration.class)
public class ReactiveResponseEntityConfig implements WebFluxConfigurer {

/**
* Creates and registers a {@link ReactiveResponseEntityHandlerResultHandler} bean within the Spring application
* context.
*
* <p>The {@code ReactiveResponseEntityHandlerResultHandler} is responsible for handling and serializing
* {@link ReactiveResponseEntity} instances in reactive web responses. It leverages the provided
* {@link ResponseEntityResultHandler} to delegate the processing of standard {@link ResponseEntity} objects,
* ensuring consistency and integration with existing response handling mechanisms.
*
* @param responseEntityHandler
* the existing {@link ResponseEntityResultHandler} bean provided by Spring WebFlux for handling standard
* {@link ResponseEntity} instances
* @return a new instance of {@link ReactiveResponseEntityHandlerResultHandler} configured with the provided
* {@code responseEntityHandler}
*/
@Bean
public ReactiveResponseEntityHandlerResultHandler reactiveResponseEntityHandlerResultHandler(
ResponseEntityResultHandler responseEntityHandler) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@

package de.kamillionlabs.hateoflux.linkbuilder;

import org.reactivestreams.Publisher;

/**
* Functional interface for method references to a controller method in the context of URI generation. This interface
* enables type-safe referencing of specific methods within a controller when constructing links with
Expand All @@ -44,5 +42,5 @@ public interface ControllerMethodReference<ControllerT> {
*
* @see SpringControllerLinkBuilder#linkTo(Class, ControllerMethodReference)
*/
Publisher<?> invoke(ControllerT controller);
Object invoke(ControllerT controller);
}
16 changes: 16 additions & 0 deletions src/main/java/de/kamillionlabs/hateoflux/model/hal/HalWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,22 @@ protected static String determineRelationNameForObject(Object object) {
}
}

/**
* Determines whether the given class represents a scalar value type.
* <p>
* Scalar types are defined as:
* <ul>
* <li>Primitive types (int, boolean, char, etc.)</li>
* <li>String</li>
* <li>Number subclasses (Integer, Double, etc.)</li>
* <li>Boolean wrapper class</li>
* <li>Character wrapper class</li>
* </ul>
*
* @param clazz
* The class to check
* @return true if the class represents a scalar type, false otherwise
*/
protected static boolean isScalar(Class<?> clazz) {
return clazz.isPrimitive()
|| clazz.equals(String.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

package de.kamillionlabs.hateoflux.dummy.controller;

import de.kamillionlabs.hateoflux.dummy.model.Book;
import de.kamillionlabs.hateoflux.http.HalResourceResponse;
import de.kamillionlabs.hateoflux.model.hal.Composite;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -97,5 +99,8 @@ public Mono<Void> postMappingWithVoidAsReturnValue() {
return Mono.empty();
}


@PostMapping("/response-type/{id}")
public HalResourceResponse<Book, Void> postMappingWithHalResponseAsReturnValue(@PathVariable String id) {
return HalResourceResponse.notFound();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ void givenRequestPutMappingWithQueryParameterAndCustomName_whenLinkTo_thenCustom


@Test
void givenpostMappingWithVoidAsReturnValue_whenLinkTo_noExceptionIsThrownAndLinkIsCorrect() {
void givenPostMappingWithVoidAsReturnValue_whenLinkTo_noExceptionIsThrownAndLinkIsCorrect() {
// GIVEN & WHEN
final Link link = linkTo(DummyController.class,
DummyController::postMappingWithVoidAsReturnValue);
Expand All @@ -193,5 +193,15 @@ void givenpostMappingWithVoidAsReturnValue_whenLinkTo_noExceptionIsThrownAndLink
assertThat(link.getHref()).isEqualTo("/dummy/void-of-nothing");
}

@Test
void givenPostMappingWithHalResponseAsReturnValue_whenLinkTo_noExceptionIsThrownAndLinkIsCorrect() {
// GIVEN & WHEN
final Link link = linkTo(DummyController.class,
c -> c.postMappingWithHalResponseAsReturnValue("123"));

//THEN
assertThat(link.getHref()).isEqualTo("/dummy/response-type/123");
}


}

0 comments on commit 611178e

Please sign in to comment.