Skip to content
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

HAL List Serializer #20

Merged
merged 10 commits into from
Sep 26, 2017
Merged

HAL List Serializer #20

merged 10 commits into from
Sep 26, 2017

Conversation

claytondaley
Copy link
Contributor

@claytondaley claytondaley commented Aug 3, 2017

This PR adds HAL-compliant list serialization to HalModelSerializers:

  • Add HalListSerializer (inherits from ListSerializer) to generate HAL-compliant output.
    • this only happens in calls to .data to avoid poisoning list of related objects
  • Update HalModelSerializer to use HalListSerializer by default (__new__ calls many_init so the change is found here)

This PR is not as clean as I'd like:

  • Unfortunately, the default list serializer is hard coded in BaseSerializer.many_init so I had to conditionally inject the new default in Meta.list_serializer_class.
    • Ideally, the default_list_serializer logic (or something similar) would be pushed up to DRF's BaseSerializer so we could just set a new default in this class.
  • There is no existing infrastructure for resolving the list view for a model
    • HalListSerializer.build_view_url parrots the approach found in DRF (smeared over a bunch of helper functions)
    • I introduced a new Meta attribute (i.e. base_name) to support manual override
    • DRF might accept base_name (or something similar) since it would support both -list and -detail.

I'm not going to actually suggest anything to DRF until the collaborators here are satisfied with the approach.

EDIT: Updated to reflect latest code.

@@ -1,14 +1,51 @@
from collections import defaultdict

import rest_framework

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'rest_framework' imported but unused

@claytondaley
Copy link
Contributor Author

Sorry. Had a face-palm moment when I realized that data and to_representation distinguished top-level from related usage.

@jstaffans jstaffans self-requested a review August 11, 2017 14:12
@jstaffans
Copy link
Contributor

jstaffans commented Aug 14, 2017

Thanks for this!

We implemented lists of links a while ago, but skipped over embedding them, in order to not blow up JSON responses too much for the default case. That's why there is a test serializer which uses many=True, but does not embed anything. I think the user should generally have to opt-in to embedding lists of related resources.

It would be interesting to hear if this PR somehow changes that, or if it's only about list-type fields that are part of a resource and not "related" to it.

A few test cases would help very much for understanding this PR!

@claytondaley
Copy link
Contributor Author

claytondaley commented Aug 14, 2017

The current state of this PR only affects the top-level representation of a collection endpoint (e.g. object-list or /objects). The PR doesn't affect nested/related lists -- probably for the same reason you state.

Here's the outline of the "conceptual" test case that fails in the current codebase:

  • Some users can create books and others cannot
  • A pure-HAL UI should be able to deduce which users can create books from the _links section. Specifically, the backend creates an entry (e.g. create) for users who are permitted to take that action.
  • However, DRF's ListSerializer returns a data structure like this (note the top-level list):
    [{
        "_links": {
            "self": {
                "href": "/library/Fight-Club"
            }
        },
        "author": "Chuck Palahniuk",
        "title":  "Fight Club"
    }, ... ]
    
  • While a _links section exists, it's actually located on the individual objects in the list. It's philosophically the wrong place for a create entry. The reason for this is easily demonstrated by the initial state (no books):
    []
    

I actually discovered the issue by looking at examples of HAL-compliant top-level collections (exemplified by this SO answer). Even for an un-paged collection, they indicate a barebones format like:

{
    "_links": {
        "self": {
            "href": "/library"
        }
    },
    "_embedded": {
        "item": [{
            "_links": {
                "self": {
                    "href": "/library/Fight-Club"
                }
            },
            "author": "Chuck Palahniuk",
            "title":  "Fight Club"
        }, ... ]
    }
}

Obviously, this structure includes a _links section where a create link could be included. The PR ensures that collections based on the HalModelSerializer are rendered with a HAL-compliant top-level structure (i.e. a _links section with a self link and an embedded section where objects are found).

@claytondaley
Copy link
Contributor Author

I'll try to find time to add test cases as well. The test harness is... less unittest than I'm used... to so there's a non-trivial learning curve there too.

@jstaffans
Copy link
Contributor

jstaffans commented Aug 14, 2017

Yeah, the test harness is one of the things that is still implemented as it was in the original repo. We'll try to clean it up when time permits..

To your ListSerializer issue: what if you just use HalModelSerializer? If you run the "abundant resources" test case, the response is rendered along the lines of HAL:

{
  "_links": {
    "self": {
      "href": "http://testserver/abundant-resources/"
    },
    "next": {
      "href": "http://testserver/abundant-resources/?cursor=cD0yMDE3LTA4LTE0KzIwJTNBMzQlM0E1NS4wMDcxNzU%3D"
    }
  },
  "_embedded": {
    "items": [
      {
        "_links": {
          "self": {
            "href": "http://testserver/abundant-resources/50/"
          }
        },
        "name": "Abundant Resource 49"
      },
      ...
   }
}

Does this solve your issue?

@claytondaley
Copy link
Contributor Author

claytondaley commented Aug 14, 2017

I don't think so. In DRF:

  1. A single item is returned as a dict
  2. A paginated collection is (usually) returned as a dict (to support prev and next)
  3. An unpaginated collection is returned as a list

Because your test case includes "next", I'm pretty sure it's an example of (2). My PR is intended to address (3).

FWIW I dislike that DRF behaves this way. I think it's confusing that adding pagination requires a rewrite of the UI, but I've reported far more serious violations of REST semantics that are still unresolved. I know better than to tilt with that DRF windmill, but this package should definitely conform to spec.

edit: if prev/next links are placed in headers (not the default), the body of a paginated DRF response may be rendered as a list as well

@jstaffans
Copy link
Contributor

Gotcha, I agree that the response should always be a dict. If you take a look at the test case again, though, the first assertion is for a resource that is not paginated (no prev or next links). This also renders as a dict.

I could however reproduce your issue by turning off pagination for the test project completely, in settings.py: 'PAGE_SIZE' = None. I imagine the test case for this needs to override the project settings.

@claytondaley
Copy link
Contributor Author

Yes. I think "no pages" is testing the case where all of the items fit on one page. The resource is still paginated (technically speaking) but no links are rendered. The = None case is the truly unpaginated resource example that falls back to DRF's inconsistent behavior.

@claytondaley claytondaley force-pushed the list-serializer branch 2 times, most recently from c10c20e to 5023139 Compare August 15, 2017 21:55
@ambsw-technology
Copy link

Test case added.

 - adapt HalModelSerializer to use HalListSerializer
…entation`)

 - use correct context object (`context` not `_context`)
 - `HalRelatedModelSerializer` to correctly render related fields (i.e. without `embedded`)
 - fix test harness to use `HalRelatedModelSerializer`
…de different structures to root (`data`) and nested (`to_representation`) uses
Copy link
Contributor

@jstaffans jstaffans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late approval, great work!

{
LINKS_FIELD_NAME: {
URL_FIELD_NAME: {
'href': self.build_view_url()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or should I be using (e.g. here):

{'href': self.request.build_absolute_uri()}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to build URI:s the same way everywhere, so I would go with self.request.build_absolute_uri(), if that is enough for your use case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It certainly looks like it should work (and eliminates the need for some extra code). At some point, we're going to need to update that code to address #27 but it's not urgent/blocker. I'll push the change.

@jstaffans jstaffans merged commit 9306146 into Artory:master Sep 26, 2017
@claytondaley claytondaley deleted the list-serializer branch September 26, 2017 15:37
@webjunkie webjunkie mentioned this pull request Sep 27, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

5 participants