-
Notifications
You must be signed in to change notification settings - Fork 472
Name embedded collections #175
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
Comments
Can you elaborate what you mean by that? What exactly are you referring to with "name of the collection"? Relation names are abstracted away by a You're free to simply implement a |
That can work as a workaround...but it is possible that i would have two collections of the same type with different semantic meaning. So in the HAL format i could have something like this at /customer/123
but in my protocol i might implement something like the zoom protocol to get things embedded. so a url like /customer/123?expand=purchased products&size=4 should give back something like this:
the embedded name should be the same as the link. So yes i could do this for one of the links, but the other at URL /customer/123?expand=favorite products&size=4 would be out of luck. and you could technically do both simultaniously (but paging both would be pretty strange). Personally i want to just name the embedded paged collection items or page items as someTypeNameList isn't semantically meaningful enough. Your workaround will probably get me there. |
Traverson (the JS variety) attempts to perform this optimization (look for link relation name in _embedded if it's provided). The auto-generated name does not match what would be generated in a link list, and so Traverson can't find _embedded links. |
+1 to drdamour's remarks, including the inadequacy of driving the name via types alone. It would be useful for example to be able to do something like
and have that show up as
rather than
|
+1 |
For those who didn't get Olivers answer straight away, like me: If you want the plural form of the class' name rather than uncapitalized class.getSimpleName + "List", you just need to use the EvoInflectorRelProvider instead of the standard DefaultRelProvider. If you use Spring Boot, just add the following dependency <dependency>
<groupId>org.atteo</groupId>
<artifactId>evo-inflector</artifactId>
<version>1.2</version>
</dependency> otherwise you need to also add the following bean to your configuration class @Bean
public RelProvider relProvider() {
return new EvoInflectorRelProvider();
} These results in returning "_embedded": {
"users": [{
"name": "Marvin"
}]
} instead of "_embedded": {
"userList": [{
"name": "Marvin"
}]
} |
As documented in the reference documentation. 😄 |
lol. I totally missed the point 3. 😄 |
+1. Also add to OP's comment, what's the rationale to have PagedResources returned under _embedded with class name? Is it possible to return just the array of data like non-HAL format? Like for "_embedded":
[
{
"name": "Marvin"
},
{
"name": "Alex"
}
] This way client doesn't have to know the property name to look for under _embedded. |
The HAL specification. |
Even if we can have, for example the name 'users' instead of the name 'userResourceList', we still have a name varying for each resource type. It'd be helpful to configure a static name, common to all resource types, like 'items' for example. |
@stephaneeybert having one static name for all collections would be very easy with the solution that @olivergierke suggested first. Implement your custom RelProvider which will always return To be honest, I would not do that because the client is supposed to know what it was asking for anyway. And a resource type reflects a resource which itself should be identifiable and recognizable. Having everything named items sounds like the complete opposite of that. |
@marvinrichter I just found out about the |
That depends on what you want to achieve. If there are only some resources you want to name But if you want to have all collections named I for myself like the pluralized forms and use the EVO Inflector. So instead of having |
Given the project supports both annotation overrides and writing your own RelProvider, I’m closing this issue. |
@gregturn the specific scenario I outlined in |
@drdamour It would like to read a proper english with no obvious spelling mistakes, and if possible, punctuation and accentuation. As to your request, may I suggest you provide the maintainer with a bit more explaination ? |
Again, using EvoInflectorRelProvider as a guide, write your own. Or use @relation. |
@gregturn as outlined in #175 (comment) neither of those suggestions work when you need two of the same type embedded within the same resource, there's no way to distinguish between them. perhaps if @relation was on the getter or field instead of just the type it could work. |
@stephaneeybert what is missing from #175 (comment) ? |
@drdamour I'm reading your comment again and again to try to understand what you want to do. For example; that phrase "but in my protocol i might implement something like the zoom protocol to get things embedded." is not so easy to grasp. From what I could guess, you are trying to have in the _embedded node a list of items that doesn't match what you have in the _links node. Why do you want to filter out part of the content of the _embedded node ? Can't you leave that to the client consuming the API response ? |
zoom protocol just allows client have more control on what is and is not embedded, and irrelevant to the underlying issue but was a real world example i had at the time. if you look again I'll think you'll see everything embedded in the example was also a link and not divergent as suggested by you. again the issue is the available rel defining methods are limited to the type being embeeded, and I often have the same type being embedded for distinct rels. eg favorite-products and purchased-products on a user resource |
RelProvider and the annotation give you the power to name the collection. Link rels you control. Therefore you can do all this. |
@gregturn can you explain how since relprovider's signature only has the class as a variable (and not something like the field name in it's method signature)...how can I return a different rel for two collections of the same type? |
Here's how you'd create an class First {
public String getName() {
return "name";
}
}
class Second {
public String getOtherName() {
return "otherName";
}
}
EmbeddedWrappers wrappers = new EmbeddedWrappers(false);
List<Object> elements = new ArrayList<>();
LinkRelation relation = LinkRelation.of("common");
elements.add(wrappers.wrap(new First(), relation));
elements.add(wrappers.wrap(new Second(), relation));
CollectionModel<Object> model = new CollectionModel<>(elements); For the HAL module this renders: {
"_embedded" : {
"common" : [ {
"name" : "name"
}, {
"otherName" : "otherName"
} ]
}
} |
but my problem is the reverse...two getters of the SAME type with different rels. |
looks like EmbeddedWrappers would work for that case too now. cool. |
Yes, if an explicit |
trying it out
gave me
so that is pretty nice...but when switched the return to thanks! |
ah yeah i see now, changing to RepresentationalModel and changing the getter to public
content property is special to jackson serializer looks like...but getContent is there on entity model too. oh man i'm gonna have to forget so much of ResourceSupport |
I'd love to keep this focused on actual tickets and actionable items here. For general discussion and questions, please refer to StackOverflow. Short: yes, all media type specific serialization is based on the |
@drdamour I'm experimenting on a fluent API to build @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"
}
}
} |
overall it seems fine. some comments pulling from how i've used resources:
that's my immediate feedback. |
I've opened a new ticket to focus the design of the representation builder. |
@gregturn You'd have the link to the newly opened ticket ? |
I guess that’s not visible on your phone. #864. |
The Resources type (and any derived types, ie PagedResource) should allow you to name the collection that will eventually be the embedded set. It's auto named now, but the api developer should really have control over this.
The text was updated successfully, but these errors were encountered: