plexus / yaks

Ruby library for building hypermedia APIs
http://rubygems.org/gems/yaks
MIT License
236 stars 26 forks source link

Collection+JSON self link behavior #87

Open plexus opened 9 years ago

plexus commented 9 years ago

It seems we were having some issues with acceptance tests not actually working as intended. I'm working on a PR that fixes that. It seems we had some CJ behavior that's in conflict with some of the acceptance examples but that wasn't caught before, and that I think we have to revisit.

The question is how to treat "self" links. CJ has a generic "links" collection on each resource, but also a "href". At the moment we render a self link at the top level as a generic self link, and at lower levels as href. I think both of these are wrong.

@mamund clarifies here: https://groups.google.com/d/msg/collectionjson/RHcw72kHUno/hzF3WSbn-ZsJ

yes, the top link SHOULD be the collection URL, which will not always be the self URL.

In Collection+JSON a singular resource is represented as a collection of one. So the way I interpret this is: If we're rendering a "real" collection (a Yaks::CollectionResource), then render a self link as "href", and not under "links". If we render a singular resource (Yaks::Resource), render it as a self link under "links", and not as "href".

Does that make sense? @carlesjove @mamund any input you can give on this? Thanks!

mamund commented 9 years ago

if i understand the Q here... the top link SHOULD always be the collection link (collection.href). that is because rendering a "single resource" in Cj is actually a Cj collection with a single item

the self link SHOULD always appear in the links array (collection.links[{rel:"self",...},...]).

does that clarify things?

plexus commented 9 years ago

That does help somewhat. In Yaks we do make a distinction between serializing a single object or serializing a collection. In either case we first build up an intermediate "resource object", and then output formats (e.g. CJ) can work with that to the best of their ability.

So suppose I'm outputting a single object, and I have configured both a self and a collection link on it.

Yaks::Resource.new(
  links: [
    Yaks::Link.new(rel: :self, href: "/foo/15"),
    Yaks::Link.new(rel: :collection, href: "/foo")
  ]
)

this could be rendered with

  "href": "/foo",
  "links": [{"rel": "self", "href": "/foo/15"}]

whereas a collection

Yaks::CollectionResource.new(
  links: [
    Yaks::Link.new(rel: :self, href: "/foo")
  ]
)

would be

  "href": "/foo",
  "links": [{"rel": "self", "href": "/foo"}]

Does that look good?

What if a single resource does not have a collection URI? Should we consider it an error to try to output that to Collection+JSON? Or do we omit the "href"? (same for a collection without a self link).

mamund commented 9 years ago

first, the examples for single and collection look good to me. this is the same output i produce in my own examples.

also, it is valid for a representation to not have a collection.href property -- this happens when the collection is read-only. you would still emit a "self" link in the collection.links[] array, tho. this is true whether you are rendering a "single-item" collection (your Yaks::Resource) or a "multi-item" collection (your Yaks::CollectionResource).

make sense?

plexus commented 9 years ago

Yeah that makes sense. Not sure how we would expose that distinction in our existing API, but I understand the rationale.

mamund commented 9 years ago

cool. feel free to ping me for these kinds of clarification -- it reminds me that the Cj docs are lacking and working through this w/ you helps me quite a bit.

i understand about the challenge of supporting all these details -- esp. across media type designs. i really love the work you're doing here.

carlesjove commented 9 years ago

I'll have a look at this and the full discussion tonight. I'm into daddy mode at the moment!

On Friday, May 8, 2015, Mike Amundsen notifications@github.com wrote:

first, the examples for single and collection look good to me. this is the same output i produce in my own examples.

also, it is valid for a representation to not have a collection.href property -- this happens when the collection is read-only. you would still emit a "self" link in the collection.links[] array, tho. this is true whether you are rendering a "single-item" collection (your Yaks::Resource) or a "multi-item" collection (your Yaks::CollectionResource).

make sense?

— Reply to this email directly or view it on GitHub https://github.com/plexus/yaks/issues/87#issuecomment-100275423.

mamund commented 9 years ago

@carlesjove

thanks. hopefully, i've articulated the media type details well. you may need to scan the actual implementation details to make sure i've not missed something very imp. regarding Yaks itself.

BTW - was great to spend time w/ you this week in Barcelona for APIDays, thanks.

carlesjove commented 9 years ago

Well, this whole issue is really tricky, because it can be dealt in many ways. The thing here is that Cj isn't very imperative (even when it's very structured), so a bunch of responses can be valid Cj.

For the situation at hand, this one example is not to passing:

{
  "collection": {
    "version": "1.0",
    "href": "http://www.youtypeitwepostit.com/api/",
    "items": []

The reason is because a CollectionMapperthat sets a self link expects a links object:

class CollectionMapper < Yaks::CollectionMapper
  link :self, 'http://www.youtypeitwepostit.com/api/'
{
  "collection": {
    "version": "1.0",
    "href": "http://www.youtypeitwepostit.com/api/",
    "links": [
      {"href": "http://www.youtypeitwepostit.com/api/", "rel": "self"}
    ]
    "items": []

I think both responses are valid Cj, so we can either:

If we look at another shared example, the Plants collection, we're expecting the second case:

{
  "collection": {
    "version": "1.0",
    "href": "http://api.example.com/plants",
    "items": [...],
    "links": [
      { "href": "http://api.example.com/plants", "rel": "self" },
      { "href": "http://api.example.com/doc/plant_collection", "rel": "profile" }
    ]
  }
}

So well, why not leave it as it is, for now, and just change the expectation? That's consistent with what we are already expecting for the Plans collection and implies zero refactoring.

plexus commented 9 years ago

Sure, we can leave everything as is, or we can make it better :) The current implementation will render a self link at the top level under "links", but will skip self links in lower levels. That's already inconsistent.

Also, while what we generate is structurally valid CJ, it's not always semantically valid. When rendering a singular resource, its self link should not be used to populate "href", because "href" is reserved for the URL of the collection.

I've been thinking we could add metadata to links to differentiate between links that are simply used as identifiers, and others that can actually be looked up. I'm just not sure what to call it. resolvable or dereferentiable both seem a bit wordy :)

class FooMapper < Yaks::Mapper
  link :self, '/api/foo', resolvable: false # indicates that GET /api/foo will *not* work

If the link to the collection is not "resolvable" it should not be used as collection.href

mamund commented 9 years ago

yes, i'd like to take best advantage of the library here and improve as much as possible.

the collection.href property is always a URL (locator, resolvable), not a URI (identifier, not-always-resolvable). it appears in responses to tell clients which URL is valid for POST-ing a new item to the collection. for this reason it SHOULD always appear in responses. the only exception is when the collection is read-only.

to be clear the self link indicates the URL of the current response and, while not a REQUIRED element, it MAY appear in the collection.links array. As a rule, i always include the selflink in that array. it would be absolutely fine if Yaks always emitted a self URL in the collection.links array.