mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-22 22:06:50 +00:00
Merge pull request #1252 from bf4/document_serialization_scope
[DOCS/TEST] Add serialization_scope example
This commit is contained in:
commit
3e0f85e9c9
@ -156,7 +156,98 @@ PR please :)
|
|||||||
|
|
||||||
#### #scope
|
#### #scope
|
||||||
|
|
||||||
PR please :)
|
Allows you to include in the serializer access to an external method.
|
||||||
|
|
||||||
|
It's intended to provide an authorization context to the serializer, so that
|
||||||
|
you may e.g. show an admin all comments on a post, else only published comments.
|
||||||
|
|
||||||
|
- `scope` is a method on the serializer instance that comes from `options[:scope]`. It may be nil.
|
||||||
|
- `scope_name` is an option passed to the new serializer (`options[:scope_name]`). The serializer
|
||||||
|
defines a method with that name that calls the `scope`, e.g. `def current_user; scope; end`.
|
||||||
|
Note: it does not define the method if the serializer instance responds to it.
|
||||||
|
|
||||||
|
That's a lot of words, so here's some examples:
|
||||||
|
|
||||||
|
First, let's assume the serializer is instantiated in the controller, since that's the usual scenario.
|
||||||
|
We'll refer to the serialization context as `controller`.
|
||||||
|
|
||||||
|
| options | `Serializer#scope` | method definition |
|
||||||
|
|-------- | ------------------|--------------------|
|
||||||
|
| `scope: current_user, scope_name: :current_user` | `current_user` | `Serializer#current_user` calls `controller.current_user`
|
||||||
|
| `scope: view_context, scope_name: :view_context` | `view_context` | `Serializer#view_context` calls `controller.view_context`
|
||||||
|
|
||||||
|
We can take advantage of the scope to customize the objects returned based
|
||||||
|
on the current user (scope).
|
||||||
|
|
||||||
|
For example, we can limit the posts the current user sees to those they created:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class PostSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :title, :body
|
||||||
|
|
||||||
|
# scope comments to those created_by the current user
|
||||||
|
has_many :comments do
|
||||||
|
object.comments.where(created_by: current_user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Whether you write the method as above or as `object.comments.where(created_by: scope)`
|
||||||
|
is a matter of preference (assuming `scope_name` has been set).
|
||||||
|
|
||||||
|
##### Controller Authorization Context
|
||||||
|
|
||||||
|
In the controller, the scope/scope_name options are equal to
|
||||||
|
the [`serialization_scope`method](https://github.com/rails-api/active_model_serializers/blob/d02cd30fe55a3ea85e1d351b6e039620903c1871/lib/action_controller/serialization.rb#L13-L20),
|
||||||
|
which is `:current_user`, by default.
|
||||||
|
|
||||||
|
Specfically, the `scope_name` is defaulted to `:current_user`, and may be set as
|
||||||
|
`serialization_scope :view_context`. The `scope` is set to `send(scope_name)` when `scope_name` is
|
||||||
|
present and the controller responds to `scope_name`.
|
||||||
|
|
||||||
|
Thus, in a serializer, the controller provides `current_user` as the
|
||||||
|
current authorization scope when you call `render :json`.
|
||||||
|
|
||||||
|
**IMPORTANT**: Since the scope is set at render, you may want to customize it so that `current_user` isn't
|
||||||
|
called on every request. This was [also a problem](https://github.com/rails-api/active_model_serializers/pull/1252#issuecomment-159810477)
|
||||||
|
in [`0.9`](https://github.com/rails-api/active_model_serializers/tree/0-9-stable#customizing-scope).
|
||||||
|
|
||||||
|
We can change the scope from `current_user` to `view_context`.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
class SomeController < ActionController::Base
|
||||||
|
+ serialization_scope :view_context
|
||||||
|
|
||||||
|
def current_user
|
||||||
|
User.new(id: 2, name: 'Bob', admin: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
user = User.new(id: 1, name: 'Pete')
|
||||||
|
render json: user, serializer: AdminUserSerializer, adapter: :json_api
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
We could then use the controller method `view_context` in our serializer, like so:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
class AdminUserSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :name, :can_edit
|
||||||
|
|
||||||
|
def can_edit?
|
||||||
|
+ view_context.current_user.admin?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
So that when we render the `#edit` action, we'll get
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"data":{"id":"1","type":"users","attributes":{"name":"Pete","can_edit":true}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `can_edit` is `view_context.current_user.admin?` (true).
|
||||||
|
|
||||||
#### #read_attribute_for_serialization(key)
|
#### #read_attribute_for_serialization(key)
|
||||||
|
|
||||||
|
|||||||
@ -96,6 +96,8 @@ module ActiveModel
|
|||||||
_serializer_instance_methods.include?(name)
|
_serializer_instance_methods.include?(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: Fix load-order failures when different serializer instances define different
|
||||||
|
# scope methods
|
||||||
def self._serializer_instance_methods
|
def self._serializer_instance_methods
|
||||||
@_serializer_instance_methods ||= (public_instance_methods - Object.public_instance_methods).to_set
|
@_serializer_instance_methods ||= (public_instance_methods - Object.public_instance_methods).to_set
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,63 +1,230 @@
|
|||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
require 'pathname'
|
|
||||||
|
|
||||||
class DefaultScopeNameTest < ActionController::TestCase
|
module SerializationScopeTesting
|
||||||
class UserSerializer < ActiveModel::Serializer
|
class User < ActiveModelSerializers::Model
|
||||||
|
attr_accessor :id, :name, :admin
|
||||||
def admin?
|
def admin?
|
||||||
current_user.admin
|
admin
|
||||||
end
|
|
||||||
attributes :admin?
|
|
||||||
end
|
|
||||||
|
|
||||||
class UserTestController < ActionController::Base
|
|
||||||
protect_from_forgery
|
|
||||||
|
|
||||||
before_action { request.format = :json }
|
|
||||||
|
|
||||||
def current_user
|
|
||||||
User.new(id: 1, name: 'Pete', admin: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_new_user
|
|
||||||
render json: User.new(id: 1, name: 'Pete', admin: false), serializer: UserSerializer, adapter: :json_api
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
class Comment < ActiveModelSerializers::Model
|
||||||
|
attr_accessor :id, :body
|
||||||
|
end
|
||||||
|
class Post < ActiveModelSerializers::Model
|
||||||
|
attr_accessor :id, :title, :body, :comments
|
||||||
|
end
|
||||||
|
class PostSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :title, :body, :comments
|
||||||
|
|
||||||
tests UserTestController
|
def body
|
||||||
|
"The 'scope' is the 'current_user': #{scope == current_user}"
|
||||||
|
end
|
||||||
|
|
||||||
def test_default_scope_name
|
def comments
|
||||||
get :render_new_user
|
if current_user.admin?
|
||||||
assert_equal '{"data":{"id":"1","type":"users","attributes":{"admin?":false}}}', @response.body
|
[Comment.new(id: 1, body: 'Admin')]
|
||||||
end
|
else
|
||||||
end
|
[Comment.new(id: 2, body: 'Scoped')]
|
||||||
|
end
|
||||||
class SerializationScopeNameTest < ActionController::TestCase
|
end
|
||||||
class AdminUserSerializer < ActiveModel::Serializer
|
|
||||||
def admin?
|
def json_key
|
||||||
current_admin.admin
|
'post'
|
||||||
end
|
end
|
||||||
attributes :admin?
|
end
|
||||||
end
|
class PostTestController < ActionController::Base
|
||||||
|
attr_accessor :current_user
|
||||||
class AdminUserTestController < ActionController::Base
|
def render_post_by_non_admin
|
||||||
protect_from_forgery
|
self.current_user = User.new(id: 3, name: 'Pete', admin: false)
|
||||||
|
render json: new_post, serializer: serializer, adapter: :json
|
||||||
serialization_scope :current_admin
|
end
|
||||||
before_action { request.format = :json }
|
|
||||||
|
def render_post_by_admin
|
||||||
def current_admin
|
self.current_user = User.new(id: 3, name: 'Pete', admin: true)
|
||||||
User.new(id: 2, name: 'Bob', admin: true)
|
render json: new_post, serializer: serializer, adapter: :json
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_new_user
|
private
|
||||||
render json: User.new(id: 1, name: 'Pete', admin: false), serializer: AdminUserSerializer, adapter: :json_api
|
|
||||||
end
|
def new_post
|
||||||
end
|
Post.new(id: 4, title: 'Title')
|
||||||
|
end
|
||||||
tests AdminUserTestController
|
|
||||||
|
def serializer
|
||||||
def test_override_scope_name_with_controller
|
PostSerializer
|
||||||
get :render_new_user
|
end
|
||||||
assert_equal '{"data":{"id":"1","type":"users","attributes":{"admin?":true}}}', @response.body
|
end
|
||||||
|
class PostViewContextSerializer < PostSerializer
|
||||||
|
def body
|
||||||
|
"The 'scope' is the 'view_context': #{scope == view_context}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def comments
|
||||||
|
if view_context.controller.current_user.admin?
|
||||||
|
[Comment.new(id: 1, body: 'Admin')]
|
||||||
|
else
|
||||||
|
[Comment.new(id: 2, body: 'Scoped')]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
class DefaultScopeTest < ActionController::TestCase
|
||||||
|
tests PostTestController
|
||||||
|
|
||||||
|
def test_default_serialization_scope
|
||||||
|
assert_equal :current_user, @controller._serialization_scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_default_serialization_scope_object
|
||||||
|
assert_equal @controller.current_user, @controller.serialization_scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_default_scope_non_admin
|
||||||
|
get :render_post_by_non_admin
|
||||||
|
expected_json = {
|
||||||
|
post: {
|
||||||
|
id: 4,
|
||||||
|
title: 'Title',
|
||||||
|
body: "The 'scope' is the 'current_user': true",
|
||||||
|
comments: [
|
||||||
|
{ id: 2, body: 'Scoped' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}.to_json
|
||||||
|
assert_equal expected_json, @response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_default_scope_admin
|
||||||
|
get :render_post_by_admin
|
||||||
|
expected_json = {
|
||||||
|
post: {
|
||||||
|
id: 4,
|
||||||
|
title: 'Title',
|
||||||
|
body: "The 'scope' is the 'current_user': true",
|
||||||
|
comments: [
|
||||||
|
{ id: 1, body: 'Admin' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}.to_json
|
||||||
|
assert_equal expected_json, @response.body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
class SerializationScopeTest < ActionController::TestCase
|
||||||
|
class PostViewContextTestController < PostTestController
|
||||||
|
serialization_scope :view_context
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def serializer
|
||||||
|
PostViewContextSerializer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tests PostViewContextTestController
|
||||||
|
|
||||||
|
def test_defined_serialization_scope
|
||||||
|
assert_equal :view_context, @controller._serialization_scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_defined_serialization_scope_object
|
||||||
|
assert_equal @controller.view_context.class, @controller.serialization_scope.class
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_serialization_scope_non_admin
|
||||||
|
get :render_post_by_non_admin
|
||||||
|
expected_json = {
|
||||||
|
post: {
|
||||||
|
id: 4,
|
||||||
|
title: 'Title',
|
||||||
|
body: "The 'scope' is the 'view_context': true",
|
||||||
|
comments: [
|
||||||
|
{ id: 2, body: 'Scoped' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}.to_json
|
||||||
|
assert_equal expected_json, @response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_serialization_scope_admin
|
||||||
|
get :render_post_by_admin
|
||||||
|
expected_json = {
|
||||||
|
post: {
|
||||||
|
id: 4,
|
||||||
|
title: 'Title',
|
||||||
|
body: "The 'scope' is the 'view_context': true",
|
||||||
|
comments: [
|
||||||
|
{ id: 1, body: 'Admin' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}.to_json
|
||||||
|
assert_equal expected_json, @response.body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# FIXME: Has bugs. See comments below and
|
||||||
|
# https://github.com/rails-api/active_model_serializers/issues/1509
|
||||||
|
class NilSerializationScopeTest < ActionController::TestCase
|
||||||
|
class PostViewContextTestController < ActionController::Base
|
||||||
|
serialization_scope nil
|
||||||
|
|
||||||
|
attr_accessor :current_user
|
||||||
|
|
||||||
|
def render_post_with_no_scope
|
||||||
|
self.current_user = User.new(id: 3, name: 'Pete', admin: false)
|
||||||
|
render json: new_post, serializer: PostSerializer, adapter: :json
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: run test when
|
||||||
|
# global state in Serializer._serializer_instance_methods is fixed
|
||||||
|
# def render_post_with_passed_in_scope
|
||||||
|
# self.current_user = User.new(id: 3, name: 'Pete', admin: false)
|
||||||
|
# render json: new_post, serializer: PostSerializer, adapter: :json, scope: current_user, scope_name: :current_user
|
||||||
|
# end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def new_post
|
||||||
|
Post.new(id: 4, title: 'Title')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tests PostViewContextTestController
|
||||||
|
|
||||||
|
def test_nil_serialization_scope
|
||||||
|
assert_nil @controller._serialization_scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_nil_serialization_scope_object
|
||||||
|
assert_nil @controller.serialization_scope
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: change to NoMethodError and match 'admin?' when the
|
||||||
|
# global state in Serializer._serializer_instance_methods is fixed
|
||||||
|
def test_nil_scope
|
||||||
|
if Rails.version.start_with?('4.0')
|
||||||
|
exception_class = NoMethodError
|
||||||
|
exception_matcher = 'admin?'
|
||||||
|
else
|
||||||
|
exception_class = NameError
|
||||||
|
exception_matcher = /admin|current_user/
|
||||||
|
end
|
||||||
|
exception = assert_raises(exception_class) do
|
||||||
|
get :render_post_with_no_scope
|
||||||
|
end
|
||||||
|
assert_match exception_matcher, exception.message
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: run test when
|
||||||
|
# global state in Serializer._serializer_instance_methods is fixed
|
||||||
|
# def test_nil_scope_passed_in_current_user
|
||||||
|
# get :render_post_with_passed_in_scope
|
||||||
|
# expected_json = {
|
||||||
|
# post: {
|
||||||
|
# id: 4,
|
||||||
|
# title: 'Title',
|
||||||
|
# body: "The 'scope' is the 'current_user': true",
|
||||||
|
# comments: [
|
||||||
|
# { id: 2, body: 'Scoped' }
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
# }.to_json
|
||||||
|
# assert_equal expected_json, @response.body
|
||||||
|
# end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user