diff --git a/README.md b/README.md index 3b42a07ef..6ff4af8f4 100644 --- a/README.md +++ b/README.md @@ -274,7 +274,9 @@ updating the attribute. See the [Value Formatters](#value-formatters) section fo ##### Flattening a Rails relationship -It is possible to flatten Rails relationships into attributes by using getters and setters. This can become handy if a relation needs to be created alongside the creation of the main object which can be the case if there is a bi-directional presence validation. For example: +It is possible to flatten Rails relationships into attributes by using getters and setters. This can become handy if a +relation needs to be created alongside the creation of the main object which can be the case if there is a +bi-directional presence validation. For example: ```ruby # Given Models @@ -339,7 +341,8 @@ end ##### Custom resource key validators -If you need more control over the key, you can override the #verify_key method on your resource, or set a lambda that accepts key and context arguments in `config/initializers/jsonapi_resources.rb`: +If you need more control over the key, you can override the #verify_key method on your resource, or set a lambda that +accepts key and context arguments in `config/initializers/jsonapi_resources.rb`: ```ruby JSONAPI.configure do |config| @@ -793,6 +796,31 @@ Will get you the following payload by default: } ``` +#### Resource Meta + +Meta information can be included for each resource. The `meta` information to include with each resource as it is +serialized is set using the `meta` method in the resource declaration. For example: + +```ruby +class BookResource < JSONAPI::Resource + attribute :title + attribute :isbn + + meta :copyright, 'Copyright 2015' + meta :computed_copyright, -> (options) {options[:serialization_options][:copyright]} + meta :last_updated_at, -> (options) {options[:source][:updated_at]} +end + +``` + +The `meta` function takes a `key` name, which will be formatted and used as the `key` in the serialized `meta` section +for the resource. It also takes a value which can either be a fixed value or a lambda which takes a single options hash. +The lambda will be called as the resource is being serialized. The `options` hash will contain the following: + +* `:source` -> the resource instance being serialized +* `:serializer` -> the serializer class +* `:serialization_options` -> the contents of the `serialization_options` method on the controller. + #### Callbacks `ActiveSupport::Callbacks` is used to provide callback functionality, so the behavior is very similar to what you may be @@ -925,7 +953,24 @@ class PeopleController < ApplicationController end ``` -> __Note__: This gem [uses the filter chain to set up the request](https://github.com/cerebris/jsonapi-resources/issues/458#issuecomment-143297055). In some instances, variables that are set in the filter chain (such as `current_user`) may not be set at the right time. If this happens (i.e. `current_user` is `nil` in `context` but it's set properly everywhere else), you may want to have your authentication occur earlier in the filter chain, using `prepend_before_action` instead of `before_action`. +Additional options can be passed to the serializer using the `serialization_options` method. + +For example: + +```ruby +class ApplicationController < JSONAPI::ResourceController + def serialization_options + {copyright: 'Copyright 2015'} + end +end +``` + +These `serialization_options` are passed to the lambda used to generate resource `meta` values. + +> __Note__: This gem [uses the filter chain to set up the request](https://github.com/cerebris/jsonapi-resources/issues/458#issuecomment-143297055). +In some instances, variables that are set in the filter chain (such as `current_user`) may not be set at the right time. +If this happens (i.e. `current_user` is `nil` in `context` but it's set properly everywhere else), you may want to have +your authentication occur earlier in the filter chain, using `prepend_before_action` instead of `before_action`. ##### ActsAsResourceController diff --git a/lib/jsonapi/acts_as_resource_controller.rb b/lib/jsonapi/acts_as_resource_controller.rb index 09dcf3f40..d5169a06d 100644 --- a/lib/jsonapi/acts_as_resource_controller.rb +++ b/lib/jsonapi/acts_as_resource_controller.rb @@ -104,6 +104,10 @@ def context {} end + def serialization_options + {} + end + # Control by setting in an initializer: # JSONAPI.configuration.json_key_format = :camelized_key # JSONAPI.configuration.route = :camelized_route @@ -159,7 +163,8 @@ def create_response_document(operation_results) base_meta: base_meta, base_links: base_response_links, resource_serializer_klass: resource_serializer_klass, - request: @request + request: @request, + serialization_options: serialization_options ) end diff --git a/lib/jsonapi/operation.rb b/lib/jsonapi/operation.rb index 5aebaeac6..9d8859b82 100644 --- a/lib/jsonapi/operation.rb +++ b/lib/jsonapi/operation.rb @@ -79,9 +79,9 @@ def initialize(resource_klass, options = {}) def apply key = @resource_klass.verify_key(@id, @context) - resource_record = resource_klass.find_by_key(key, - context: @context, - include_directives: @include_directives) + resource_record = @resource_klass.find_by_key(key, + context: @context, + include_directives: @include_directives) return JSONAPI::ResourceOperationResult.new(:ok, resource_record) diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index c4285ac5a..e1a32ea1f 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -263,6 +263,7 @@ def inherited(base) base.abstract(false) base.immutable(false) base._attributes = (_attributes || {}).dup + base._meta = (_meta || {}).dup base._relationships = (_relationships || {}).dup base._allowed_filters = (_allowed_filters || Set.new).dup @@ -283,7 +284,7 @@ def resource_for(type) resource end - attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator + attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator, :_meta def create(context) new(create_model, context) @@ -370,6 +371,11 @@ def primary_key(key) @_primary_key = key.to_sym end + def meta(name, value) + @_meta ||= {} + @_meta[name] = value + end + # TODO: remove this after the createable_fields and updateable_fields are phased out # :nocov: def method_missing(method, *args) diff --git a/lib/jsonapi/resource_serializer.rb b/lib/jsonapi/resource_serializer.rb index 73ee8b7f9..d0d50a45d 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -1,6 +1,9 @@ module JSONAPI class ResourceSerializer + attr_reader :url_generator, :key_formatter, :serialization_options, :primary_class_name + + # initialize # Options can include # include: # Purpose: determines which objects will be side loaded with the source objects in a linked section @@ -10,9 +13,7 @@ class ResourceSerializer # relationship ids in the links section for a resource. Fields are global for a resource type. # Example: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]} # key_formatter: KeyFormatter class to override the default configuration - # base_url: a string to prepend to generated resource links - - attr_reader :url_generator + # serializer_options: additional options that will be passed to resource meta and links lambdas def initialize(primary_resource_klass, options = {}) @primary_class_name = primary_resource_klass._type @@ -25,6 +26,7 @@ def initialize(primary_resource_klass, options = {}) JSONAPI.configuration.always_include_to_one_linkage_data) @always_include_to_many_linkage_data = options.fetch(:always_include_to_many_linkage_data, JSONAPI.configuration.always_include_to_many_linkage_data) + @serialization_options = options.fetch(:serialization_options, {}) end # Converts a single resource, or an array of resources to a hash, conforming to the JSONAPI structure @@ -119,9 +121,32 @@ def object_hash(source, include_directives) relationships = relationship_data(source, include_directives) obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty? + meta = resource_meta(source) + obj_hash['meta'] = meta unless meta.nil? || meta.empty? + obj_hash end + def custom_generation_options(source) + { + resource: source, + serializer: self, + serialization_options: @serialization_options + } + end + + def resource_meta(source) + return nil if source.class._meta.empty? + + meta = {} + source.class._meta.each_pair do |k, v| + value = v.respond_to?(:call) ? v.call(custom_generation_options(source)) : v + meta[format_key(k)] = value unless value.nil? + end + + meta + end + def requested_fields(klass) return if @fields.nil? || @fields.empty? if @fields[klass._type] diff --git a/lib/jsonapi/response_document.rb b/lib/jsonapi/response_document.rb index 65dcb356b..de1c970c3 100644 --- a/lib/jsonapi/response_document.rb +++ b/lib/jsonapi/response_document.rb @@ -36,7 +36,8 @@ def serializer fields: @options[:fields], base_url: @options.fetch(:base_url, ''), key_formatter: @key_formatter, - route_formatter: @options.fetch(:route_formatter, JSONAPI.configuration.route_formatter) + route_formatter: @options.fetch(:route_formatter, JSONAPI.configuration.route_formatter), + serialization_options: @options.fetch(:serialization_options, {}) ) end diff --git a/test/controllers/controller_test.rb b/test/controllers/controller_test.rb index 4333451b8..74a1bbc88 100644 --- a/test/controllers/controller_test.rb +++ b/test/controllers/controller_test.rb @@ -2264,6 +2264,15 @@ def test_get_person_as_author assert_equal nil, json_response['data'][0]['attributes']['email'] end + def test_show_person_as_author + get :show, {id: '1'} + assert_response :success + assert_equal '1', json_response['data']['id'] + assert_equal 'authors', json_response['data']['type'] + assert_equal 'Joe Author', json_response['data']['attributes']['name'] + assert_equal nil, json_response['data']['attributes']['email'] + end + def test_get_person_as_author_by_name_filter get :index, {filter: {name: 'thor'}} assert_response :success @@ -2271,6 +2280,33 @@ def test_get_person_as_author_by_name_filter assert_equal '1', json_response['data'][0]['id'] assert_equal 'Joe Author', json_response['data'][0]['attributes']['name'] end + + def test_computed_meta_serializer_options + JSONAPI.configuration.json_key_format = :camelized_key + + Api::V5::AuthorResource.meta :fixed, 'Hardcoded value' + Api::V5::AuthorResource.meta :computed, + -> (options) {"#{options[:resource].class._type.to_s}: #{options[:serializer].url_generator.self_link(options[:resource])}"} + Api::V5::AuthorResource.meta :computed_foo, + -> (options) {options[:serialization_options][:foo]} + + get :show, {id: '1'} + assert_response :success + assert_equal '1', json_response['data']['id'] + assert_equal 'authors', json_response['data']['type'] + assert_equal 'Joe Author', json_response['data']['attributes']['name'] + assert_equal 'Hardcoded value', json_response['data']['meta']['fixed'] + assert_equal 'authors: http://test.host/api/v5/authors/1', json_response['data']['meta']['computed'] + assert_equal 'bar', json_response['data']['meta']['computedFoo'] + + ensure + JSONAPI.configuration.json_key_format = :dasherized_key + + Api::V5::AuthorResource.meta :fixed, nil + Api::V5::AuthorResource.meta :computed, nil + Api::V5::AuthorResource.meta :computed_foo, nil + end + end class BreedsControllerTest < ActionController::TestCase diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index c27adf0de..7ab3108cc 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -683,6 +683,9 @@ class BooksController < JSONAPI::ResourceController module V5 class AuthorsController < JSONAPI::ResourceController + def serialization_options + {foo: 'bar'} + end end class PostsController < JSONAPI::ResourceController @@ -1253,6 +1256,15 @@ def self.find(filters, options = {}) return resources end + def self.find_by_key(key, options = {}) + context = options[:context] + records = records(options) + records = apply_includes(records, options) + model = records.where({_primary_key => key}).first + fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil? + self.new(model, context) + end + def fetchable_fields super - [:email] end diff --git a/test/unit/serializer/serializer_test.rb b/test/unit/serializer/serializer_test.rb index 75e5928bf..3ac6810f7 100644 --- a/test/unit/serializer/serializer_test.rb +++ b/test/unit/serializer/serializer_test.rb @@ -1737,6 +1737,67 @@ def test_serializer_to_one ) end + def test_serializer_resource_meta_fixed_value + Api::V5::AuthorResource.meta :fixed, 'Hardcoded value' + Api::V5::AuthorResource.meta :computed, + -> (options) {"#{options[:resource].class._type.to_s}: #{options[:serializer].url_generator.self_link(options[:resource])}"} + + serialized = JSONAPI::ResourceSerializer.new( + Api::V5::AuthorResource, + include: ['author_detail'] + ).serialize_to_hash(Api::V5::AuthorResource.new(Person.find(1), nil)) + + assert_hash_equals( + { + data: { + type: 'authors', + id: '1', + attributes: { + name: 'Joe Author', + }, + links: { + self: '/api/v5/authors/1' + }, + relationships: { + posts: { + links: { + self: '/api/v5/authors/1/relationships/posts', + related: '/api/v5/authors/1/posts' + } + }, + authorDetail: { + links: { + self: '/api/v5/authors/1/relationships/authorDetail', + related: '/api/v5/authors/1/authorDetail' + }, + data: {type: 'authorDetails', id: '1'} + } + }, + meta: { + fixed: 'Hardcoded value', + computed: 'authors: /api/v5/authors/1' + } + }, + included: [ + { + type: 'authorDetails', + id: '1', + attributes: { + authorStuff: 'blah blah' + }, + links: { + self: '/api/v5/authorDetails/1' + } + } + ] + }, + serialized + ) + ensure + Api::V5::AuthorResource.meta :fixed, nil + Api::V5::AuthorResource.meta :computed, nil + end + def test_serialize_model_attr @make = Make.first serialized = JSONAPI::ResourceSerializer.new(