Links: a relation for actions #745

Open
hhware opened this Issue Jun 4, 2015 · 11 comments

Projects

None yet

8 participants

@hhware
Contributor
hhware commented Jun 4, 2015

I suggest to register with IANA a relation to identify actions, as AFAIK there is no existing relation for this yet. The motivation is that an API client should have a way to extract all actions from the list of links for a given resource, whether they are standardized or custom.

Suggestion for relation name: action.

Ref: (8) of #684

@ethanresnick
Member

Hey @hhware! Thanks for opening this. I'm sorry I'm just responding to it now. I've been working on other projects post 1.0, so I haven't been able to give much time to these conversations, but I do want to discuss this.

First, I really like the goal that you're trying to achieve here, which I take to be making all the non-standard actions that a resource supports discoverable. However, I'm not sure links is the right place for this.

Before explaining my position, though, I want to clarify how I'm using the term "resource" below, since I know it has (at least) two meanings—the JSON API one and the HTTP one—and our understandings of each meaning may be different. I apologize in advance if my use of this terminology is already familiar to you (or others), or if I'm misunderstanding the term's meaning, but I just want to make sure we're on the same page before we start.

Ok, so, when I talk about an HTTP resource, I mean a concept (e.g. "the 1988 baseball world series winner" or "the movies Brad Pitt has starred in"). This concept is always a noun and is identified by a URI. So, the first resource might be identified by http://example.com/word-series-winners/1988 and the second by http://example.com/movies?actor=Brad%20Pitt. The second resource would, in JSON API speak, be a "collection", since it represents a set of movies. But, from the HTTP perspective, that multiple movies are in the set doesn't matter; the set of movies is is still a single resource. Then, because these URIs/resources represent nouns, the verbs (GET, POST, etc.) are used to retrieve or act on them.

Now, back to an action link relation. Basically, I think:

First, that many "non-standard" actions can actually be reformulated as standard actions on new HTTP resources, if the resources are designed properly. Then, these actions can be advertised at the protocol level (i.e. with the Allow header) and with just a standard link to the new resource (on which the action exists), rather than a custom "action" link.

Second, there will still probably be some actions that can't be reformulated in terms of a standard HTTP verb on a new resource. However, I suspect that invoking those actions will, almost by definition, require the client to construct a non-standard request body. So (generic) clients will need help to know how to do that; just an action URI and a method will be insufficient. Therefore, what these clients need is really more sophisticated hypermedia controls (a la write templates), rather than simple link relations.

Let me try to make these points more concrete, using the example actions from #684 and #773.

  • First, there are those actions that I think can be reformulated in terms of standard actions on new HTTP resources. One example of this is version-drop-wip. In the example in #684, the client triggers the version-drop-wip action by POSTing to http://example.com/articles/2/version-drop-wip. There are a couple problems here: first, how should the client know what to POST (an issue I'll come back to later); and, second, http://example.com/articles/2/version-drop-wip isn't a great resource in the HTTP sense, because it's not really a noun (unless you say the noun is "the action of deleting the latest version", in which case I'm not sure it's a very useful noun).

    A more conventional resource would be "the work-in-progress version", which then might be identified by the URI http://example.com/articles/2/version-wip. Now, suddenly, the version-drop-wip action just becomes applying the DELETE method to the aforementioned URI. This is what I mean by remapping seemingly non-standard actions to standard verbs on new resources.

    Note that minting the "the work-in-progress version" as a resource and giving it a URI doesn't mean that URI has to respond to the GET method (it can 405, for instance), so we haven't created an extra implementation burden to the server. But what we have done is define a more-conventional noun, which means we can also repurpose existing link relations (here, latest-version), which were designed to work on these conventional nouns.

    So we turn:

    "--version-drop-wip": {
      "rel": ["action", "http://example.com/notions/version-drop-wip"],
      "method": "post",
      "href": "http://example.com/articles/2/version-drop-wip"
    }
    

    Into:

    "latest-version": {
      "methods": ["delete"],
      "href": "http://example.com/articles/2/version-wip"
    }
    

    (Here "methods" is just a list of the methods that the server is claiming will likely to be available on the linked resource.)

    This second version is more understandable by generic clients, since it uses only the standard latest-version relation, rather than custom and/or new ones. It's also more convenient, since it uses a DELETE request, which is easier to form than a POST. Finally, it's more concise and extensible (e.g. one could easily add "get" to the methods list, and all would work).

    So, my first point is that I think many custom actions follow this pattern: they can be reformatted as noun-ier resources; those resources can show the methods they support in the methods key and in the Allow header; existing link relations will then fit better; and an "action" relation will be unnecessary, since the action is implied by the standard verb.

    Other clear examples of this are the examples given in #773. POST /shopping_cart/buy can become POST /shopping_cart/relationships/items, which doesn't need to be described at all, since JSON API defines what to POST. Likewise, /users/1/sendmessage becomes POST /users/1/relationships/messages. And /user/login can probably be reformulated in a similar manner (e.g. POST /login-attempts), with the details depending on the circumstances.

  • But then there are the second type of actions, which I don't think can be solved just by minting new resources and linking to those. For example, the --publish action from #684. Whether that action is formulated as a POST to a ..../publish URI or a PATCH to the article's self URI (as I would suggest), the core problem remains: what should the client POST/PATCH to invoke the action? I see two bad options:

    1. The client could be configured in advance, using information outside of the API, to know what to send in the request body to invoke the action. The problem with this is that, if it's being configured externally for the particular API, it could've been manually told about the existence of the --publish action—it didn't need the link at all.
    2. Assuming the /publish URI is used, the server could allow the client to POST anything (or even an empty payload) to trigger the action. The problem here, though, is that this isn't a generic solution, since it doesn't leave the client any way to pass arguments to the action it's trying to invoke (which some actions need). Also, it forecloses the "PATCH the self link" option.

This leads me to my contention that what we need to describe custom actions like this is hypermedia controls, rather than an action link relation. For example, if we had a hypermedia control for publishing, we could include that on the self link, like this:

{
   // below, note that including "patch" and "delete" in "methods"
   // covers the --update and --delete actions from #684 too
   "links": {
      "self": {
         "href": "http://example.com/articles/1",
         "methods": ["get", "patch", "delete"],
         "actions": [{
            "name": "publish",
            "method": "patch",
            "fields": [{"name": "published", "value": true}]
         }]
      }
   },
   // ... rest of article resource ...
}

Where the above hypermedia control says "trigger the publish action by sending a PATCH to the self link that contains the "published" field set to true".

A similar approach would be to put the actions outside of "links", in a separate "actions" key. This is what Siren does, and it has its pros and cons. We could even not output any actions in the response payload, and instead expose the actions for each resource as the response to an OPTIONS request to its URI.

But my big point is: for actions that are more nuanced than the CRUD ones that JSON API defines, I agree that we definitely need a way tell clients that those actions exist and how to form requests for invoking them. But I don't think an "action" relation is the answer, since that would imply having dedicated "action" URIs, which shouldn't be necessary. Such URIs violate the idea of "resources as nouns", which is baked into much of HTTP—not just existing link relations, but also things like HTTP caching. (For example, a POST to /articles/1/publish won't invalidate a cache's copy of /articles/1, but a PATCH /articles/1 will.)

When I have more time, I'll try to write up my thoughts on how to provide these hypermedia controls. In the mean time, I'm curious to hear if you agree with this general read and approach forward, and if you have any ideas for how the hypermedia controls should work.

@ksimka
Contributor
ksimka commented Jun 22, 2015

You can hate me, but I repeat it again: I'm not against machine-readable documentation, it's cool, but I'm strongly against mixing API with its machine-readable docs. Please keep it separated. "Do one thing and do it well" — remember? There are other tools for API actions discovery and docs (like Swagger already mentioned earlier).

I'm watching how all around are trying to make a monster from a small well-done tool, and it's make me sad.

@sapeien
Contributor
sapeien commented Jun 22, 2015

@ksimka There are some machine-readable documentations that are useful at runtime, such as JSON Schema, which are of a completely different category than Swagger, RAML, API Blueprint, etc, which are more like documentation as the engine of application state.

Still I'm not sure what is the purpose of having such an affordance as "actions" that can't be modelled with standard HTTP methods.

@hhware
Contributor
hhware commented Jun 27, 2015

@ethanresnick, thanks for reviving this with a detailed analysis!

I totally agree that some non-standard actions can be reformatted in terms of more standard actions on different resources. --version-drop-wip was just an example, maybe not the best one, and definitely application-specific (e.g., I did not suggest it for inclusion in #749). But I think that it would be wrong to force the API designer to do so.

the core problem remains: what should the client POST/PATCH to invoke the action

Let me start with this part of the question: what is actually the core problem here. Discoverable actions, at least the way I suggest to implement them, do not pretend to solve the problem of building full hypermedia APIs, which need little to no documentation. It is assumed that the documentation on formats of specific requests is needed. The purpose of actions is to inform the client what is available for the given user on a given resource at a given moment of time -- this is why actions should be returned dynamically, as part of a resource's representation, and not via schema, docs, or in some other static way. So to me, the core problem is different from what you describe.

The other related aspect of it is organizational: when this feature become available in the base spec. Having in mind that this feature does not make into 1.1 and that the (current) plan is to tackle only a couple major features per release, I have a concern that if we dive deep into hypermedia stuff, discoverable actions will not make it into 1.2 either. For me, discoverable actions is a very practical feature with a well-defined scope, whereas support for full hypermedia APIs are an interesting (mostly) theoretical area with somewhat uncertain scope, and which can be handled in different ways, i.e., dynamically via hypermedia controls, statically via schema, etc., and it is not clear at this point which of them or what combination of them should JSON API support.

If there were a way to make sure that both these things could make it into 1.2, I would love to explore both concepts together. However, there does not seem to be a way to ensure anything like that in a community-driven project, so my strong preference would be to tackle discoverable actions first, having full hypermedia in mind, and worry about the rest of full hypermedia later or in parallel, but not bundle them together.

In short, for the purposes of this effort, I suggest to assume that "what should the client POST/PATCH" is known from elsewhere.

it could've been manually told about the existence of the --publish action — it didn't need the link at all.

As noted above, I view the purpose of discoverable actions differently. IMHO, the "manual" solution addresses neither the permission aspect ("the given user") nor the temporal/workflow/lifecycle/etc aspect ("at the given moment of time"), unless I misunderstand the power of that "manual" solution...

Here "methods" is just a list of the methods that the server is claiming will likely to be available on the linked resource

Agree, it may be a good idea to turn methods into an array. E.g., a single action --modify could cover PUT, PATCH, DELETE on a resource, especially if the given API does not provide separate permissions for them. But I think it should be up to the API designer whether to combine multiple actions under the same link.

This leads me to my contention that what we need to describe custom actions like this is hypermedia controls

Please correct me if I am wrong, but I think that both your and my suggestions can be classified as hypermedia controls. Mine is action-name-centric (actions can be identified by unique link names), one-level, URI may need to be repeated. Yours is link-URI-centric, two-level, actions will can be made unique or may have to be idenfified by a combination of link name and action name -- BTW, what is your view on this? I think mine is easier to understand and use, but both ultimately convey the same information, as long as there is an ability to assign identifiable names to each action (or group of actions), which the API client can use.

A similar approach would be to put the actions outside of "links", in a separate "actions" key.

This is what I started with in #590. It is definitely an option, although I do not see why it is better than placing them in links. It depends on semantics of links: what are they? In my view, action is a type of link. The advantage of placing all links under one key is that each given link can have several classifications at the same time, e.g., it can be an action related to versioning of the given resource. This approach seems to be in line with one taken in RFC5988.

We could even not output any actions in the response payload, and instead expose the actions for each resource as the response to an OPTIONS request to its URI.

Use of OPTIONS requires that, if we have more actions available on a given resource than we have HTTP verbs, we would have to spread them over multiple associated resource URIs and therefore will not be able to return all actions in one trip, which is a requirement of some APIs. Also, what about compound documents? I think OPTIONS is just not flexible enough for what JSON API already supports.

But I don't think an "action" relation is the answer, since that would imply having dedicated "action" URIs, which shouldn't be necessary. Such URIs violate the idea of "resources as nouns", which is baked into much of HTTP

IMHO, this is the domain of API designer, not JSON API spec. The spec is agnostic about URI structure in general, why should it enforce anything related to it when actions are addressed?

For example, a POST to /articles/1/publish won't invalidate a cache's copy of /articles/1, but a PATCH /articles/1 will.

Are concerns of cache invalidation in the scope of the spec? IMHO, it is an implementation-specific question. In complex APIs, approaches along the lines of Linked Cache Invalidation might be needed...

In the mean time, I'm curious to hear if you agree with this general read and approach forward,

As I mentioned above, I think that the structure proposed by me is easier to understand. I think that if we turn method in #684 into methods, my suggestion becomes closer to yours. Perhaps we can try to combine them in a way that it would be up to the API designer to decide whether to spread an URI over several links or combine all actions with the same URI under one link? E.g., an example of profiles in #622 (comment) seems to be a good use case for multiple URIs under the same action.

I do not really have time now to work on specific suggestion, but will definitely try to do it later. (FYI: I will mostly be offline for about 3 weeks, unlikely to participate in any activity here during this period.)

and if you have any ideas for how the hypermedia controls should work.

If you mean specifying request format, write templates, etc, I tried to address it above. If not, please let me know what you meant.

I'm not against machine-readable documentation

@ksimka, you can hate me, but I repeat it again: the main purpose of actions is to be able to inform the API client what the given user can do with the given resource or relationships at the given moment of time. Documentation is static, actions are dynamic. Their use cases overlap, but only partially.

I'm watching how all around are trying to make a monster from a small well-done tool, and it's make me sad.

IMHO, it depends on what kind of tool JSON API wants to be. E.g., to me, a combination wrench (especially a brand new polished chrome-vanadium one) looks strikingly beautiful compared to a clumsy adjustable wrench. Nonetheless, I bought my first adjustable wrench long before I bought my first set of combination wrenches, just because of its utility...

Still I'm not sure what is the purpose of having such an affordance as "actions" that can't be modelled with standard HTTP methods.

@daliwali, is RFC5988 a "standard HTTP" in your opinion?

@ethanresnick
Member

@hhware Thanks for your detailed reply, Alex! I understand your use case better now, and I think it will be possible to find a way to support it before we get full hypermedia support.

I also don't have much time over the next few weeks, so I won't be able to write up a detailed response for a while. But I won't forget this issue, nor will the rest of the team.

@Kukunin
Kukunin commented Aug 6, 2015

I support @hhware in this conversation.

First, there definitely may custom actions for a resource, which can't be 'refactored' in terms of standard HTTP verbs. For example, we have a Task resource, which has a accept and reject actions.

Second, OPTIONS HTTP method is not an option, because it supports only HTTP verbs in the Allow: header. So, I can't specify in the OPTIONS /tasks/1 response, that you can accept, but can't reject this specific task.

Third, As it was said already, actions are dynamic. For example, I can't DELETE this document, because parent's task is archived already, but can DELETE another document, because its task is still open. I want to keep business logic only in one place: on the API backend.

@belison
belison commented Aug 20, 2015

Thanks Ethan for reaching out to us. Our chief architect has put a ton of thought into hyper, here is one of his projects https://github.com/hypergroup/hyper-json. The readme has a lot of examples. We have been using this pattern for about a year and have really loved it.

@ethanresnick
Member

@belison Thanks for jumping in! I'm taking a look at hyper now, and will definitely take it's ideas into account when we restart this discussion in full force (right now, we're trying to finish #795 and #650 first).

@benoittgt benoittgt added a commit to benoittgt/json-api that referenced this issue Aug 28, 2015
@benoittgt @benoittgt benoittgt + benoittgt Add recommendations for asynchronous processing
Started a discussion on discourse about it  : http://discuss.jsonapi.org/t/long-running-jobs-or-asynchronous-processing/26

Maybe the status response have too much infos for this example.
Maybe "actions" should not be there.

Move "status" and "errors" in an "attributes" bucket

Temporary remove "actions" container. Waiting for standard about it.

Waiting json-api#745

Remove errors fields

Move queue link to content location, add it to 'attributes'

Remove links self and grammar corrections

Move Header/location to Content-Location
d474956
@asteinlein

Any progress or new info related to this discussion?

@Kukunin
Kukunin commented Mar 11, 2016

I would like to hear about links for actions as well, be a use I think it's great way to keep business logic in one place and avoid duplication it on client.

@aimeos
aimeos commented Dec 11, 2016

One small suggestions when adding allowed methods for links to handle permissions:
The HTTP header that can be sent for any resource is "Allow: GET, PATCH, DELETE" so the link attribute should be instead

"latest-version": {
  "href": "http://example.com/articles/2/version-wip"
  "allow": ["GET", "PATCH", "DELETE"],
}

to be as close as possible to the HTTP header. Only "Allow" should lower-case to be in line with all other keys in the JSON payload.

@aimeos aimeos added a commit to aimeos/json-api that referenced this issue Dec 11, 2016
@aimeos aimeos Added "allow" attribute for links to restrict access
This PR relates to the discussion in json-api#745
If access to a resource is limited by a permission system, the HTTP header that can be sent along with the resource is "Allow: GET, PATCH, DELETE". For included resources, the link attribute should be able to list the allowed methods the same way:
```
"links": {
  "related": {
    "href": "http://example.com/articles/1/comments",
    "allow": ["GET", "PATCH", "DELETE"]
  }
}
```
to be as close as possible to the HTTP header. Only "Allow" should lower-case to be in line with all other keys in the JSON payload.
10ba7eb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment