cerebris / jsonapi-resources

A resource-focused Rails library for developing JSON:API compliant servers.
http://jsonapi-resources.com
MIT License
2.32k stars 530 forks source link

Support Meta #89

Closed lgebhardt closed 8 years ago

lgebhardt commented 9 years ago

JR should support meta members. JSON API now allows meta in the top level, in a resource, and in a link object. Details of both custom meta information and standard JR meta information (if any) still needs to be defined.

bwinterling commented 9 years ago

:+1: for meta/ pagination.

jamonholmgren commented 9 years ago

:+1:

lgebhardt commented 9 years ago

We have PR #202 and PR #199 which both get us farther along towards support for meta tags. I appreciate the work done on these PRs and I see some good ideas in them. However I'm not sure they go far enough to account for the various uses of the meta sections.

As I see it the different meta sections need to be controlled/generated at different levels.

Top Level: This meta is the domain of the request (which could include the resource type, the filters, and the paginator). For example the total_count field discussed in #199 needs to take into account filters used in the request, at least in some use cases. It may also be desirable to provide the total record count for a resource type, though that may need to account for permissions. I think the meta fields would be defined on the controller, and from here class methods on the resources could also be called.

Resource level: This meta is the domain of the resource instance. I see registering meta fields in the resource by key name and a method on the resource instance to call.

Links: This meta is the domain of the association. I can see total_count being useful here as well in a has_many association.

At each of these levels I'd like to allow actual meta entries to be fully configurable by allowing any meta key to relate to a function which will be called with a defined set of parameters/options.

Finally I see defining some standard methods on the resource and controller that could be used by default. An obvious candidate would be to use the core of the find method that puts together the relation and use it for returning the count.

I'd love to hear opinions on this. I'm sure I've missed some big things so feel free to suggest them or to poke holes in this proposal.

jamonholmgren commented 9 years ago

I'm currently handling meta information this way, which doesn't solve everything but works for my needs. I override the index method on my base controller:

class Api::V2::ApiController < JSONAPI::ResourceController
  # ...
  def index
    serializer = JSONAPI::ResourceSerializer.new(resource_klass,
                                                 include: @request.include,
                                                 fields: @request.fields,
                                                 base_url: base_url,
                                                 key_formatter: key_formatter,
                                                 route_formatter: route_formatter)

    resource_records = resource_klass.find(resource_klass.verify_filters(@request.filters, context),
                                           context: context,
                                           sort_criteria: @request.sort_criteria,
                                           paginator: @request.paginator)

    meta = {
      meta: {
        page: {
          number: @request.paginator.number,
          size:   @request.paginator.size,
          total:  page_total
        }
      }
    }
    render json: meta.merge(serializer.serialize_to_hash(resource_records))
  rescue => e
    handle_exceptions(e)
  end

  private

  def page_total
    (item_total.to_f / @request.paginator.size).ceil
  end

  def item_total
    resource_klass.records(context: context).size
  end

  # ...
end

I'd like to see a more formal way to handle this within jsonapi-resources, though.

slaskis commented 9 years ago

So it seems to me that #202 (sans the class-method support) would handle the resource meta and the controller and/or request should instead provide some kind of api (maybe they just need to respond_to meta which return a hash?) for paginators, filters and sorters to append meta fields.

Not sure where and how to deal with links though, they aren't really represented by anything "public" at the moment, unless I've missed something.

slaskis commented 9 years ago

In the mean time I think I'll steal @jamonholmgren idea of hijacking the index action to be able to get page count and result count.

lgebhardt commented 9 years ago

I agree that #202 would be a good model for the resource meta. I'm struggling a bit with the request meta. It seems that the contents of meta could vary based on the action. For example we don't need a page section for the show action, but we may want the copyright info for everything.

For links we do have an association that gets created behind the scenes. Meta configuration could be stored there, and set as you declare either has_one or has_many. But that could certainly be cumbersome.

akharris commented 9 years ago

I'm a little late to this conversation but I think the request-level metadata could be resolved by a combination of overwriting ActsAsResourceController#base_response_meta to contain the meta information that you want returned on every request and by defining a Resource.find_meta method (similar to Resource.find_count) that returns a hash of information based on the filters and options based in. That way each Operation type could make a Resource.respond_to?(:#{operation_type}_meta) method, such find_meta for a FindOperation, or a show_meta for a ShowOperation, etc, and add them to the options on the way to creating a ResourcesOperationResult object.

Or, in ruby psuedo-code

class SomeController < JSONAPI::Controller

  # is returned in the meta key for every response
  def base_response_meta
    {
      copyright: "blah blah blah",
      authors: [
        "Mark Twain",
        "Ernest Hemmingway"
      ]
    }
  end
end

class SomeResource < JSONAPI::Resource
  attributes :title, :body

  def self.find_meta(filters, options)
    {
      query_params: {
        page_num: options[:paginator].number,
        page_size: options[:paginator].size
      }
    }
  end
end

# in FindOperation
class FindOperation < Operation
# ...

  def find_meta
    @_find_meta ||= @resource_klass.find_meta(@resource_klass.verify_filters(@filters, @context),
                            context: @context,
                            sort_criteria: @sort_critera,
                            paginator: @paginator)
  end

  def apply
    resource_records = @resource_klass.find #...

    options = {}
    if JSONAPI.configuration.top_level_links_include_pagination
      options[:pagination_params] = pagination_params
    end
    if JSONAPI.configuration.top_level_meta_include_record_count
      options[:record_count] = record_count
    end

    if @resource_klass.respond_to?(:find_meta)
      options[:meta] = find_meta
    end

    return JSONAPI::ResourcesOperationResult.new(:ok, resource_records, options)
  end
end

so now a request to http://example.com/somes returns

{
  data: [<collection of objects>],
  meta: {
    copyright: "blah blah blah",
    authors: [
      "Mark Twain",
      "Ernest Hemmingway"
    ],
    query_params: {
      page_num: 1
      page_size: 10
    }
  }
}

whereas a request to http://example.com/somes/1 returns

{
  data: {< single object>},
  meta: {
    copyright: "blah blah blah",
    authors: [
      "Mark Twain",
      "Ernest Hemmingway"
    ]
  }
}
patrickgordon commented 8 years ago

Would love to see support for this - would be super handy.

barelyknown commented 8 years ago

For anyone that comes across this requirement in the future, here's a solution (and then I'll close the issue).

class FooController < JSONAPI::ResourceController
  def base_meta
    super.merge(
      bar: "baz"
    )
  end
end

Then all responses for all methods on that controller will include "bar": "baz" in the top level "meta".

Ross-Hunter commented 7 years ago

For future searchers that find this issue: if you need the result set in order to calculate something for the meta key, the way to do it is via a Processor -> http://jsonapi-resources.com/v0.9/guide/operation_processors.html