spring-projects / spring-hateoas

Spring HATEOAS - Library to support implementing representations for hyper-text driven REST web services.
https://spring.io/projects/spring-hateoas
Apache License 2.0
1.03k stars 476 forks source link

HAL-FORMS: problems with the inlining mecanism #1188

Open samihus opened 4 years ago

samihus commented 4 years ago

The HAL-FORMS specification do not use the inlining mecanism, and rather recommends separating HAL and HAL FORMS documents. If I understood the implementation by Spring HATEOAS in this example, I think it reduces the features that can be offered by an API.

Let's consider an example with a resource "issue" (/issues/{id}). When performing a GET, I want to see its details + some hypermedia controls to tell me how to "update the details", how to "assign" it, how to "cancel" it and how to "mark it resolved". And I want all this actions to be performed in a "command style" resources and not as direct updates of the "issue" resource.

The actual implemented iniling do not allow this design... because there is no way to reference an URI from the _template, it's all implicitly linked to the self relation.

If the template object was inlined into a link object, then it would be possible to do that.

{
    "_links": {
        "self": {
            "href": "/issues/1",
            "_templates": {
                 "default": {
                      "title":  "update",
                      "method" : "PUT"

                 }
            }
        },
        "assignCommand" : {
            "href" :  "/issues/1/assign-commands",
            "_templates": {
                "default" : {
                      "title":  "assign",
                      "method" : "POST",
                      "other" :" Attributes"
                 }
            }
        },
        "resolveCommand" : {
            "href" :  "/issues/1/resolve-commands",
            "_templates": {
                "default" : {
                      "title":  "resolve",
                      "method" : "POST",
                      "other" :" Attributes"
                 }
            }
        },
        "cancelCommand" : {
            "href" :  "/issues/1/cancel-commands",
            "_templates": {
                "default" : {
                      "title":  "cancel",
                      "method" : "POST",
                      "other" :" Attributes"
                 }
            }
        }
    }
}
odrotbohm commented 4 years ago

According to the HAL FORMS spec, _templates is a top-level property.

samihus commented 4 years ago

According to the HAL FORMS spec, _templates is a top-level property.

True, but in a HAL FORMS Document. If we consider that HAL FORMS document is identified by the relation in the Link object of the HAL document, then inlining it should be under the link object it refers to. Not at a global level...

mamund commented 4 years ago

why not include more than one element in _templates?

{
  "_links: {...},
  "_templates" : {
    "self" : { ... },
    "assignCommand" : { ... },
    "resolveCommand" : { ... }, 
    "cancelCommand" : { ...}
  }
}
samihus commented 4 years ago

why not include more than one element in _templates?

Because the actions are performed on different URIs, I didn't found in the specification any reference inside the template object to a target URI. So I assumed it implicitly refers to the HAL Link object's URI.

mamund commented 4 years ago

you can use the href that appears in the associated link object inline.

{
  "_links: {...},
    "assignCommand" : { "href" : "/issues/1/assign-commands", ...},
    "resolveCommand" : { "href" :  "/issues/1/resolve-commands", ...},
    "cancelCommand" : { "href" :  "/issues/1/assign-commands", ...}
  }
  "_templates" : {
    "self" : { ... },
    "assignCommand" : { ... },
    "resolveCommand" : { ... }, 
    "cancelCommand" : { ...}
  }
}

@odrotbohm : i assume what i am describing here is possible w/ the spring implementation. is that right?

samihus commented 4 years ago

you can use the href that appears in the associated link object inline

Do you mean that using the same name for the template key and the link key makes the mapping between the HAL Link Object and the JSON-FORMS Template Object ?

IOW, if I find assignCommand in the links, then to know how to invoke it I should look at the assignCommand template ?

mamund commented 4 years ago

yep. that's how it works for stand-alone HAL-FORMS, too. or you can use the _htarget query string value to add/override that.

gregturn commented 4 years ago

@mamund Currently, Spring HATEOAS's implementation looks for the self link, and from there looks for all "affordances". So no, we can't assemble multiple templates if that template is based on a different URI.

This was based on the fact that all the flows in the spec showed the self relation being the central one. Along with:

For this release, the only valid link object is the self link object.

We can certainly adjust things to look for affordance amidst ALL the links.

gregturn commented 4 years ago

Inside HalFormsTemplateBuilder, if I replace...

List<Affordance> affordances = resource.getLink(IanaLinkRelations.SELF) //
        .map(Link::getAffordances) //
        .orElse(Collections.emptyList());

...with...

List<Affordance> affordances = resource.getLinks().stream()
    .flatMap(link -> link.getAffordances().stream())
    .collect(Collectors.toList());

...then this...

RepresentationModel<?> model = new RepresentationModel<>();

Link selfLink = Affordances.of(Link.of("/issues/1")) //
        .afford(HttpMethod.PUT) //
        .withName("update") //
        .toLink();

Link assignCommand = Affordances.of(Link.of("/issue/1/assign-commands", LinkRelation.of("assignCommand")))
    .afford(HttpMethod.POST) //
    .withName("assignCommand") //
    .toLink();

Link resolveCommand = Affordances.of(Link.of("/issue/1/resolve-commands", LinkRelation.of("resolveCommand")))
    .afford(HttpMethod.POST) //
    .withName("resolveCommand") //
    .toLink();

Link cancelCommand = Affordances.of(Link.of("/issue/1/cancel-commands", LinkRelation.of("cancelCommand")))
    .afford(HttpMethod.POST) //
    .withName("cancelCommand") //
    .toLink();

model.add(selfLink, assignCommand, resolveCommand, cancelCommand);

System.out.println(this.mapper.writeValueAsString(model));

...will yield this HAL-FORMS document...

{
  "_links" : {
    "self" : {
      "href" : "/issues/1"
    },
    "assignCommand" : {
      "href" : "/issue/1/assign-commands"
    },
    "resolveCommand" : {
      "href" : "/issue/1/resolve-commands"
    },
    "cancelCommand" : {
      "href" : "/issue/1/cancel-commands"
    }
  },
  "_templates" : {
    "default" : {
      "method" : "put",
      "properties" : [ ]
    },
    "cancelCommand" : {
      "method" : "post",
      "properties" : [ ]
    },
    "resolveCommand" : {
      "method" : "post",
      "properties" : [ ]
    },
    "assignCommand" : {
      "method" : "post",
      "properties" : [ ]
    }
  }
}
arthur-noseda commented 4 years ago

This last change looks very promising. The result seems to match Mike Amundsen's design. Do you know if it will be featured into an upcoming release? I built the project with the fix. Surprisingly, no unit test was broken. Do you need help with these?

carlobeltrame commented 3 years ago

Oh, it wasn't clear to me at all from the HAL-FORMS specification that each _template is supposed to correspond to a specific _link. My understanding was that the target URL of the HTTP request defined by a template is supposed to be defined in the target property of the template, or will fall back to the document's self link otherwise. In other words, when writing a client, I see no incentive to assume there might be a link with a rel matching the command I'm using, when there are the target attribute and the self link that serve as fallback system. If there is a matching link as well as a target property, how would I know which one should be used?

carlobeltrame commented 3 years ago

Ah, I see the target property has been implemented in the meantime in https://github.com/spring-projects/spring-hateoas/commit/752b000e6c7cc3dd3d5e03ed012dee87857117f4 for issue #1427. This means that this issue is more or less obsolete, correct?