Skip to content

Create a dedicated builder for HAL representations #864

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

Closed
gregturn opened this issue Mar 12, 2019 · 21 comments
Closed

Create a dedicated builder for HAL representations #864

gregturn opened this issue Mar 12, 2019 · 21 comments
Labels
in: core Core parts of the project in: mediatypes Media type related functionality type: enhancement
Milestone

Comments

@gregturn
Copy link
Contributor

gregturn commented Mar 12, 2019

@drdamour I'm experimenting on a fluent API to build RepresentationModel objects. What do you think of something like this for building an embedded structure like you depicted up above?

@RestController
static class ProductController {

	LinkRelation favoriteProducts = LinkRelation.of("favorite products");
	LinkRelation purchasedProducts = LinkRelation.of("purchased products");

	@GetMapping("/products")
	public RepresentationModel<?> all() {

		EmbeddedModelBuilder builder = ModelBuilder //
				.embed() //
				.rootLink(linkTo(methodOn(ProductController.class).all()).withSelfRel());

		PRODUCTS.keySet().stream() //
				.map(id -> new EntityModel<>(PRODUCTS.get(id), new Link("http://localhost/products/{id}").expand(id))) //
				.forEach(productEntityModel -> {

					if (productEntityModel.getContent().isFavorite()) {

						builder //
								.embed(favoriteProducts) //
								.entityModel(productEntityModel) //
								.rootLink(productEntityModel.getRequiredLink(SELF).withRel(favoriteProducts));
					}

					if (productEntityModel.getContent().isPurchased()) {

						builder //
								.embed(purchasedProducts) //
								.entityModel(productEntityModel) //
								.rootLink(productEntityModel.getRequiredLink(SELF).withRel(purchasedProducts));
					}
				});

		return builder.build();
	}
}

It yields:

{
  "_embedded" : {
    "favorite products" : [ {
      "someProductProperty" : "someValue",
      "_links" : {
        "self" : {
          "href" : "http://localhost/products/777"
        }
      }
    }, {
      "someProductProperty" : "someValue",
      "_links" : {
        "self" : {
          "href" : "http://localhost/products/998"
        }
      }
    } ],
    "purchased products" : [ {
      "someProductProperty" : "someValue",
      "_links" : {
        "self" : {
          "href" : "http://localhost/products/111"
        }
      }
    }, {
      "someProductProperty" : "someValue",
      "_links" : {
        "self" : {
          "href" : "http://localhost/products/222"
        }
      }
    }, {
      "someProductProperty" : "someValue",
      "_links" : {
        "self" : {
          "href" : "http://localhost/products/333"
        }
      }
    }, {
      "someProductProperty" : "someValue",
      "_links" : {
        "self" : {
          "href" : "http://localhost/products/444"
        }
      }
    }, {
      "someProductProperty" : "someValue",
      "_links" : {
        "self" : {
          "href" : "http://localhost/products/555"
        }
      }
    }, {
      "someProductProperty" : "someValue",
      "_links" : {
        "self" : {
          "href" : "http://localhost/products/666"
        }
      }
    }, {
      "someProductProperty" : "someValue",
      "_links" : {
        "self" : {
          "href" : "http://localhost/products/998"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost/products"
    },
    "purchased products" : [ {
      "href" : "http://localhost/products/111"
    }, {
      "href" : "http://localhost/products/222"
    }, {
      "href" : "http://localhost/products/333"
    }, {
      "href" : "http://localhost/products/444"
    }, {
      "href" : "http://localhost/products/555"
    }, {
      "href" : "http://localhost/products/666"
    }, {
      "href" : "http://localhost/products/998"
    } ],
    "favorite products" : [ {
      "href" : "http://localhost/products/777"
    }, {
      "href" : "http://localhost/products/998"
    } ]
  }
}

A plain old collection where the embedded link relation is a based on the domain type:

@GetMapping("/authors")
RepresentationModel<?> collection() {

	return ModelBuilder //
			.collection() //

			.entity(new Author("Greg L. Turnquist", null, null)) //
			.link(linkTo(methodOn(EmbeddedController.class).authorDetails(1)).withSelfRel())
			.link(linkTo(methodOn(EmbeddedController.class).collection()).withRel("authors")) //

			.entity(new Author("Craig Walls", null, null)) //
			.link(linkTo(methodOn(EmbeddedController.class).authorDetails(2)).withSelfRel())
			.link(linkTo(methodOn(EmbeddedController.class).collection()).withRel("authors")) //

			.entity(new Author("Oliver Drotbhom", null, null)) //
			.link(linkTo(methodOn(EmbeddedController.class).authorDetails(2)).withSelfRel())
			.link(linkTo(methodOn(EmbeddedController.class).collection()).withRel("authors")) //

			.rootLink(linkTo(methodOn(EmbeddedController.class).collection()).withSelfRel()) //

			.build();
}

produces

{
  "_embedded" : {
    "authors" : [ {
      "name" : "Greg L. Turnquist",
      "_links" : {
        "self" : {
          "href" : "http://localhost/author/1"
        },
        "authors" : {
          "href" : "http://localhost/authors"
        }
      }
    }, {
      "name" : "Craig Walls",
      "_links" : {
        "self" : {
          "href" : "http://localhost/author/2"
        },
        "authors" : {
          "href" : "http://localhost/authors"
        }
      }
    }, {
      "name" : "Oliver Drotbhom",
      "_links" : {
        "self" : {
          "href" : "http://localhost/author/2"
        },
        "authors" : {
          "href" : "http://localhost/authors"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost/authors"
    }
  }
}

And a single item representation:

@GetMapping("/other-author")
RepresentationModel<?> singleItem() {

	return ModelBuilder //
			.entity(new Author("Alan Watts", "January 6, 1915", "November 16, 1973")) //
			.link(new Link("/people/alan-watts")) //
			.build();
}

produces

{
  "name" : "Alan Watts",
  "born" : "January 6, 1915",
  "died" : "November 16, 1973",
  "_links" : {
    "self" : {
      "href" : "/people/alan-watts"
    }
  }
}

Related issue: #175

@gregturn
Copy link
Contributor Author

per @Sam-Kruglov (#270 (comment)):

@GetMapping("/author/{id}")
RepresentationModel<?> authorDetails(@PathVariable int id) {

	return ModelBuilder
                      //.root(someObj) possibility to add some fields to the root is missing?
			.embed(
                          LinkRelation.of("author"), // that could be a string
	                  entity(new Author("Alan Watts", "January 6, 1915", "November 16, 1973")), // that could be an Object without call to entity(..)
			  link(new Link("/people/alan-watts")) // this is varargs as it is optional. also could be a Link without calls to link(..)
                        )
			.embed(
                          LinkRelation.of("illustrator")), 
			  entity(new Author("John Smith", null, null)), 
			  link(new Link("/people/john-smith")) 
                        )
			.link(new Link("/books/the-way-of-zen")) 
			.link(new Link("/people/alan-watts", LinkRelation.of("author"))) 
			.link(new Link("/people/john-smith", LinkRelation.of("illustrator"))) 
			.build();
}

@gregturn
Copy link
Contributor Author

per @drdamour (#175 (comment))

overall it seems fine. some comments pulling from how i've used resources:

  • i've learned from past mistakes to avoid nested embeds when possible, but does rootLink go all the way to the root, or the immediate parent...you'll probably want both. i'd be looking for .parent() and .root() more than rootLink() or parentLink()..especailly that self case on the collection, rootLink is weird there cause wy would i add a self to the root in the context of a subresource except in the case of a literal construction...in which case i'd much rather call link(new SelfLink()) before calling .collection()
  • I think of it as a special case of HAL that things are embedded with rel only. There was a poster on hal forums who asked why we don't embed by url, something that is better for compatibility with http2 and uri templates (you could embed common template completions) that i thought was pretty smart. fundamentally a sub resource can only be embedded by way of some hypermedia control, a link being the easiest by far, but templated links not far behind. Maybe it goes against a pattern of builders to have two params, but to me, i must embed a sub resource with a link together in pairs. so i'd have embed take two params, the entity and the link to embed it with (which in hal ONLY cares about the rel...but in others may care about urls or even more). But i get how that could be a pretty abstract concept, and if someone just wants to build HAL they'd question why they have to have a whole link with href just to output a rel string in the serialization.
  • rel's distancing themselves from order has always been weird to me...i get that item doesn't imply order, but is the order those rels appear in the document important? i've always thought so..and in practice it's necessary to work that way (cause order happens to matter to UIs/UX..a lot). As such, i'd be a bit concerned over the way the embed and root are added/removed in pairs in different methods to maintain order. I'd want that to be less manual and prefer it to be more an option of the builder which would do the linking from self. but even thinking of what to name that would be awful... ModelBuilder.generateLinksForRel("purchased").fromEmbeddedRel(REL_SELF).embed(thingWithSelfRel). what do you do when you do that an the embedding thing has no self rel. if embed took two params as prev suggested, then this would be much better. ModelBuilder.maintainLinksForEmbedded(true).embed(link, embeddedModel) or ModelBuilder.maintainLinksForEmbedded(false).embed(embeddedModel.getLink(REL_SELF).withRel("purchased"), embeddedModel) former would keep the _links in hal, latter would ignore them.
  • for the collection case, how do you set properties on the collection? I guess you start with entity() but then how do you get back to the entity? entity(e).collection().embed(sub1).embed(sub2).???
  • it's come up a few times where i want to embed an empty set. _embedded: { 'x': []} This indicates to the client that yes i know you wanted this sub resource...but there just isn't any..vs straight up absence which sometimes implies it just couldn't be fulfilled but go ahead and ask for it via a link. I've tried to get away form this pattern and instead have clients look for the links...but then they crash on NPEs...so i go back to making a service that is weird to help client devs that aren't safe. I think you might need to support such a case...but maybe not necessary in a builder. just a ramble about a useful anit-pattern.

that's my immediate feedback.

@gregturn
Copy link
Contributor Author

per @odrotbohm (#270 (comment))

That still feels like quite a bit of duplication? What if we started with a Link and allow users to augment this with a preview?

Link wattsLink = new Link("/people/alan-watts", LinkRelation.of("author"));
Link illustratorLink = new Link("/people/john-smith", LinkRelation.of("illustrator"));

ModelBuilder.entity(…)
  .andLink(wattsLink).withEmbed(new Author(…))
  .andLink(illustratorLink).withEmbed(new Author(…))
  .build();

That could still keep the ….embed(…) methods around if you wanted to add embeds excplitly.

@gregturn
Copy link
Contributor Author

My deep desire is for the API to flow like the HAL document itself, which is why I deliberately put .embed() first, followed by .entity() and .link(). It pushed me into the position of needing .rootLink() to discern top level links from lower level ones.

@odrotbohm
Copy link
Member

That's the tricky bit. We will have to abstract from HAL a bit to make this also usable for other media types. Maybe a HAL specific subtype could add further constraints then.

@Sam-Kruglov
Copy link

@gregturn I think it should be reflecting the JSON, that is what we are essentially building. So that is why I suggest putting these inside embed because in my mind I put them inside _embedded section

@odrotbohm
Copy link
Member

That's deceptive but be reminded that this is not a HAL specific API.

@Sam-Kruglov
Copy link

@odrotbohm regarding your example, your builder makes it so link is mandatory which it shouldn't be, at least for my use cases I didn't need them inside _embedded at all

@odrotbohm
Copy link
Member

Not necessarily. As I explained, there could be other more general methods on it but if you're starting with "Here's a link with a given relation…", it's quite natural to be immediately able to state "Oh, btw. here's the preview for the resource that just mentioned link points to.".

@Sam-Kruglov
Copy link

@odrotbohm I see. So, in your example, entity represents root fields, andLink represent the _links section and you can add things to _embedded by continuing of building the andLink and you could even reuse the relation name there. That is pretty cool, although I am not sure if you even need to provide a link to something that is embedded already

@Sam-Kruglov
Copy link

Sam-Kruglov commented Mar 13, 2019

@odrotbohm and about that being a non-HAL specific API, that would still make sense to put things inside embed rather than doing all of that at the root level of the builder since in some other format (not HAL), the embedded part would just be rendered differently but would still mean the same thing, that you are including some resources (like in JSON:API sparse fieldsets, they have included; don't know any other format that uses links though). So my logic still applies

@gregturn
Copy link
Contributor Author

I prototyped by trying to make things as flat as possible. It A) makes it easier to code a builder pattern and B) reduces the amount of "deep" reading you must undertake.

But I get the point of putting something inside another. It definitely self-groups things.

@Sam-Kruglov
Copy link

Sam-Kruglov commented Mar 13, 2019

@gregturn it would actually make the user write less code, since you can omit some of the entity,link, LinkRelation (etc) calls by just including their arguments into the embed. I would place API user over API developer, IMHO.
That of course doesn't make it bad, just suggesting 👍

@gregturn
Copy link
Contributor Author

Agree on user over developer.

So I seek maximum readability six months later.

@drdamour
Copy link

personally, i don't call it embedded....i use the terminology of a subresource in my resource model. _embedded/embedded is a hal specific concept IMO

@toedter
Copy link

toedter commented Mar 24, 2019

I like the idea having a builder that also supports the concept of sub resources. While I like @odrotbohm's idea of using this as a preview for a link relation, I would suggest also allowing sub resources bound to their parent resources directly and then contain optional links themselves.

In HAL it is very common to provide _embedded resources with a subset of attributes (like Projections in Data REST) that then provide a self link.

@gregturn gregturn added this to the 1.0 M3 milestone May 8, 2019
@odrotbohm odrotbohm modified the milestones: 1.0 M3, 1.0 RC1 Jun 13, 2019
@gregturn gregturn modified the milestones: 1.0 RC1, 1.0 RC2 Jul 31, 2019
@gregturn gregturn added in: core Core parts of the project in: infrastructure Build infrastructure and dependency upgrades labels Jul 31, 2019
@gregturn gregturn modified the milestones: 1.0.0.RC2, 1.0.0.RELEASE Sep 4, 2019
@gregturn gregturn removed this from the 1.0.0.RELEASE milestone Sep 17, 2019
gregturn added a commit that referenced this issue Apr 7, 2020
gregturn added a commit that referenced this issue Apr 7, 2020
Create an enclosing version of the builder.
gregturn added a commit that referenced this issue Apr 7, 2020
gregturn added a commit that referenced this issue Apr 7, 2020
Make a fluent API that lets you construct various RepresentationModel objects (EntityModel, CollectionModel, or complex embedded).
gregturn added a commit that referenced this issue Apr 7, 2020
gregturn added a commit that referenced this issue Apr 7, 2020
Create an enclosing version of the builder.
gregturn added a commit that referenced this issue Apr 7, 2020
gregturn added a commit that referenced this issue Apr 7, 2020
gregturn added a commit that referenced this issue Apr 17, 2020
Make a fluent API that lets you construct various RepresentationModel objects (EntityModel, CollectionModel, or complex embedded).

Related issue: #193.
@gregturn
Copy link
Contributor Author

Experimenting with the use case from #193 has exposed that my original solutions so far only work when confined to a single domain object type.

That's no good. What if you want to capture multiple types into a single representation?

So I took all the lessons learned, and rewrote the ModelBuilder. Test cases were updated, and also incorporate the use case from #193 itself as yet another test case.

Also captured tests in both WebMVC and WebFlux, verifying they work in either scenario. (Not surprising given this is REALLY just representation assembly).

If this is acceptable, I can also update the reference documentation to illustrate using this new ModelBuilder.

gregturn added a commit that referenced this issue Apr 17, 2020
Make a fluent API that lets you construct various RepresentationModel objects (EntityModel, CollectionModel, or complex embedded).

Original pull request: #1273.
Related issue: #193.
gregturn added a commit that referenced this issue Apr 24, 2020
Create a basic Model interface with a subinterface called Builder.

Implement a DefaultModelBuilder that only focuses on entities and links. This allows building some of the simplest representations that are supported by all formats.

Implement a HalModelBuilder that supports the same basic operations but also includes HAL-specific embed() and previewFor() as HAL syntax sugar.

By having a basic interface, other media types are free to either A) go along with the default format, or B) implement their own implementation, with mediatype-specific operators.
gregturn added a commit that referenced this issue Apr 24, 2020
Defines a basic Model.Builder interface that works for all hypermedia types with option for specific mediatypes to implement a customized version.

* Implement a DefaultModelBuilder that only focuses on entities and links. This allows building some of the simplest representations that are supported by all formats.
* Implement a HalModelBuilder that supports the same basic operations but also includes HAL-specific embed() and previewFor() as HAL syntax sugar.

By having a basic interface, other media types are free to either A) go along with the default format, or B) implement their own implementation, with mediatype-specific operators.

Also, introduces a preferredMediaTypes attribute in RepresentationModel so various serializers can warn if the user is attempting to serialize a HAL-specific representation as, say, Collection+JSON.

Original pull request: #1273.
Related issue: #193.
gregturn added a commit that referenced this issue Apr 24, 2020
Defines a basic Model.Builder interface that works for all hypermedia types with option for specific mediatypes to implement a customized version.

* Implement a DefaultModelBuilder that only focuses on entities and links. This allows building some of the simplest representations that are supported by all formats.
* Implement a HalModelBuilder that supports the same basic operations but also includes HAL-specific embed() and previewFor() as HAL syntax sugar.

By having a basic interface, other media types are free to either A) go along with the default format, or B) implement their own implementation, with mediatype-specific operators.

Also, introduces a preferredMediaTypes attribute in RepresentationModel so various serializers can warn if the user is attempting to serialize a HAL-specific representation as, say, Collection+JSON.

Original pull request: #1273.
Related issue: #193.
gregturn added a commit that referenced this issue Apr 24, 2020
…els.

* Implement a `DefaultModelBuilder` that only focuses on entities and links. This allows building some of the simplest representations that are suppoerted by all formats.
* Implement a `HalModelBuilder` that supports the same basic operations but also includes HAL-specific embed() and previewFor() as HAL syntax sugar.

`DefaultModelBuilder` allows defining "simple" formats (single-item or collection at an aggregate root). A `builder()` static helper is provided to create an instance of this type.

`HalModelBuilder` allows going into HAL-specific details, like `embed()` and `previewFor`, where you can specify an entity AND it's link relation. This results in a representation that will generate an "_embedded" entry. It also marks the `RepresntationModel` object with a HAL/HAL-FORMS "preferredMediaType", allowing other serializers to either log warnings, or fail.

Having the interface grants hypermedia authors the ability to create their own customized model builders as they see fit.

Also, introduces a preferredMediaTypes attribute in RepresentationModel so various serializers can warn if the user is attempting to serialize a HAL-specific representation as, say, Collection+JSON.

Original pull request: #1273.
Related issue: #193.
gregturn added a commit that referenced this issue Apr 24, 2020
* Implement a `DefaultModelBuilder` that only focuses on entities and links. This allows building some of the simplest representations that are suppoerted by all formats.
* Implement a `HalModelBuilder` that supports the same basic operations but also includes HAL-specific embed() and previewFor() as HAL syntax sugar.

`DefaultModelBuilder` allows defining "simple" formats (single-item or collection at an aggregate root). A `builder()` static helper is provided to create an instance of this type.

`HalModelBuilder` allows going into HAL-specific details, like `embed()` and `previewFor`, where you can specify an entity AND it's link relation. This results in a representation that will generate an "_embedded" entry. It also marks the `RepresntationModel` object with a HAL/HAL-FORMS "preferredMediaType", allowing other serializers to either log warnings, or fail.

Having the interface grants hypermedia authors the ability to create their own customized model builders as they see fit.

Also, introduces a preferredMediaTypes attribute in RepresentationModel so various serializers can warn if the user is attempting to serialize a HAL-specific representation as, say, Collection+JSON.

Original pull request: #1273.
Related issue: #193.
@odrotbohm odrotbohm removed this from the 1.1.0.RC1 milestone Apr 27, 2020
@odrotbohm odrotbohm changed the title Create a builder for representations. Create a dedicated builder for HAL representations May 7, 2020
odrotbohm added a commit that referenced this issue May 7, 2020
HalModelBuilder expose HAL-idiomatic API to set up representations. That includes embeds, previews and syntactic sugar around the inclusion of potentially empty collections as embeds.

Related tickets: #175, #193, #270, #920.
Original pull request: #1273.
odrotbohm added a commit that referenced this issue May 7, 2020
Consistently use javascript instead of json for JSON code blocks in the reference documentation.
odrotbohm added a commit that referenced this issue May 7, 2020
Consistently use javascript instead of json for JSON code blocks in the reference documentation.
@odrotbohm
Copy link
Member

We took a first stab at a HAL specific model builder with 2cab91a. See the updated reference documentation for details.

@odrotbohm odrotbohm added in: mediatypes Media type related functionality type: enhancement and removed in: infrastructure Build infrastructure and dependency upgrades labels May 7, 2020
@odrotbohm odrotbohm added this to the 1.1.0.RELEASE milestone May 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Core parts of the project in: mediatypes Media type related functionality type: enhancement
Projects
None yet
Development

No branches or pull requests

6 participants