Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Your contribution here.

* [#107](https://github.com/ruby-grape/grape-active_model_serializers/pull/107): Honor namespace and `render` collection roots without changing inferred single-object roots; document adapter behavior, root precedence, and unwrapped arrays - [@dblock](https://github.com/dblock).
* [#110](https://github.com/ruby-grape/grape-active_model_serializers/pull/110): Add SimpleCov LCOV reports, merged stable-matrix Coveralls reporting, and a coverage badge; remove the obsolete Code Climate badge and refresh gem/CI badge links - [@dblock](https://github.com/dblock).
* [#109](https://github.com/ruby-grape/grape-active_model_serializers/pull/109): Document the optional third-party Swagger model parser, response model configuration, and schema limitations - [@dblock](https://github.com/dblock).
* [#106](https://github.com/ruby-grape/grape-active_model_serializers/pull/106): Update default serializer options documentation to use AMS 0.10 field filtering and cover request-level defaults and overrides - [@dblock](https://github.com/dblock).
Expand Down
52 changes: 48 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,22 @@ In this case, as User objects are being returned, grape-active_model_serializers

### Array Roots

When serializing an array, the array root is set to the innermost namespace name if there is one, otherwise it is set to the route name.
Root wrapping depends on the ActiveModelSerializers adapter. The default `:attributes` adapter ignores `root` and returns a bare array. Select the `:json` adapter to wrap the collection in a named root:

```ruby
ActiveModelSerializers.config.adapter = :json
```

Grape's `format :json` selects the response format, not the AMS adapter. The `:json_api` adapter follows JSON:API's `data` structure instead of using an arbitrary root.

With the `:json` adapter, the default array root is the innermost namespace name if there is one, otherwise it is the route name.

In the following API the array root is `users`.

```ruby
namespace :users do
get ":id" do
@user = User.find(params[:id])
get '/' do
User.all
end
end
```
Expand All @@ -102,10 +110,46 @@ In the following example the array root is `people`.

```ruby
get "people" do
@user = User.all
User.all
end
```

For collections, set `root` on a namespace or route to override the default. Inner namespace roots override outer ones, and a route option takes precedence over its namespace:

```ruby
namespace :users, root: 'members' do
get '/' do
User.all
end

get '/recent', root: 'recent_members' do
User.all
end
end
```

Pass `root` to `render` to override namespace and route options for that response, including empty collections:

```ruby
get '/people', root: 'people' do
render User.all, adapter: :json, each_serializer: UserSerializer, root: 'members'
end
```

This returns an object such as `{ "members": [{ "first_name": "Jane", "last_name": "Doe" }] }`; an empty collection returns `{ "members": [] }`.

In AMS 0.10, `root: false` does not remove the `:json` adapter's envelope. To return a bare array for one response, choose the `:attributes` adapter:

```ruby
get '/people' do
render User.all, adapter: :attributes, each_serializer: UserSerializer
end
```

Return the model collection rather than a manually constructed `ActiveModel::Serializer::CollectionSerializer`. The formatter constructs the serializer and adapter; options such as `root` only affect the final response when the adapter handles them.

Namespace roots apply only to collections. Single-object responses retain the root inferred by AMS from the serializer or model, even inside a namespace with an explicit `root`. Set `root` on the route or pass it to `render` to override a single-object root explicitly.

### API Versioning

If your Grape API is versioned you must namespace your serializers accordingly.
Expand Down
2 changes: 1 addition & 1 deletion lib/grape-active_model_serializers/endpoint_extension.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def render(resources, extra_options = {})
:meta, :meta_key
)
env['ams_adapter'] = options.slice(
:adapter, :serializer, :each_serializer, :include,
:adapter, :serializer, :each_serializer, :include, :root,
:fields, :key_transform, :links, :namespace
)
env['ams_extra'] = options[:extra]
Expand Down
10 changes: 10 additions & 0 deletions lib/grape-active_model_serializers/options_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,23 @@ def endpoint_options
[
endpoint.default_serializer_options || {},
endpoint.namespace_options,
namespace_root_options,
endpoint.route_options,
endpoint_metadata,
endpoint.options,
endpoint.options.fetch(:route_options)
].reduce(:merge)
end

def namespace_root_options
return {} unless resource.respond_to?(:to_ary)

scopes = endpoint.inheritable_setting.namespace_stackable[:namespace] || []
scopes.each_with_object({}) do |scope, options|
options.merge!(scope.options.slice(:root))
end
end

def endpoint_metadata
options = {}
options[:for] = endpoint.api if endpoint.respond_to?(:api)
Expand Down
112 changes: 112 additions & 0 deletions spec/features/grape-active_model_serializers/render_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,115 @@ def get_resource_with(meta)
end
end
end

describe 'single-object and collection roots' do
let(:app) { Class.new(Grape::API) }
let(:resources) { [User.new(first_name: 'Jane', last_name: 'Doe')] }
let(:route_options) { {} }
let(:render_options) { {} }

before do
ActiveModelSerializers.config.adapter = :json
app.format :json
app.formatter :json, Grape::Formatter::ActiveModelSerializers

resource = resources
options = render_options
route_settings = route_options
app.namespace :directory, root: 'directory_members' do
namespace :users, root: 'members' do
get '/people', **route_settings do
render resource, options
end
end
end

get '/directory/users/people'
end

it 'uses the innermost namespace root' do
expect(JSON.parse(last_response.body)).to eq(
'members' => [{ 'first_name' => 'Jane', 'last_name' => 'Doe' }]
)
end

context 'route options' do
let(:route_options) { { root: 'recent_members' } }

it 'overrides the namespace root' do
expect(JSON.parse(last_response.body)).to eq(
'recent_members' => [{ 'first_name' => 'Jane', 'last_name' => 'Doe' }]
)
end
end

context 'render options' do
let(:route_options) { { root: 'recent_members' } }
let(:render_options) { { root: 'selected_members' } }

it 'overrides the route and namespace roots' do
expect(JSON.parse(last_response.body)).to eq(
'selected_members' => [{ 'first_name' => 'Jane', 'last_name' => 'Doe' }]
)
end

context 'an empty collection' do
let(:resources) { [] }

it 'keeps the explicit root' do
expect(JSON.parse(last_response.body)).to eq('selected_members' => [])
end
end
end

context 'root is false' do
let(:render_options) { { root: false } }

it 'uses the AMS-inferred root rather than removing the JSON envelope' do
expect(JSON.parse(last_response.body)).to eq(
'users' => [{ 'first_name' => 'Jane', 'last_name' => 'Doe' }]
)
end
end

context 'the attributes adapter' do
let(:render_options) { { adapter: :attributes, root: 'ignored' } }

it 'returns a bare array regardless of the root option' do
expect(JSON.parse(last_response.body)).to eq(
[{ 'first_name' => 'Jane', 'last_name' => 'Doe' }]
)
end
end

context 'a single object' do
let(:resources) { User.new(first_name: 'Jane', last_name: 'Doe') }

it 'keeps the inferred model root despite explicit namespace roots' do
expect(JSON.parse(last_response.body)).to eq(
'user' => { 'first_name' => 'Jane', 'last_name' => 'Doe' }
)
end

context 'route options' do
let(:route_options) { { root: 'person' } }

it 'honors an explicit route root' do
expect(JSON.parse(last_response.body)).to eq(
'person' => { 'first_name' => 'Jane', 'last_name' => 'Doe' }
)
end
end

context 'render options' do
let(:route_options) { { root: 'person' } }
let(:render_options) { { root: 'selected_person' } }

it 'overrides the route root' do
expect(JSON.parse(last_response.body)).to eq(
'selected_person' => { 'first_name' => 'Jane', 'last_name' => 'Doe' }
)
end
end
end
end
Loading