Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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|
Expand Down Expand Up @@ -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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How about:

Meta information can be included for each resource 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
Expand Down Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion lib/jsonapi/acts_as_resource_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions lib/jsonapi/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 7 additions & 1 deletion lib/jsonapi/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 28 additions & 3 deletions lib/jsonapi/resource_serializer.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion lib/jsonapi/response_document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
36 changes: 36 additions & 0 deletions test/controllers/controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2264,13 +2264,49 @@ 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
assert_equal 3, json_response['data'].size
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
Expand Down
12 changes: 12 additions & 0 deletions test/fixtures/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions test/unit/serializer/serializer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down