Skip to content
Merged
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 @@ -7,6 +7,7 @@
* [#41](https://github.com/ruby-grape/grape-roar/pull/41): Document hash-backed representers with explicit key getters and verify module/decorator rendering for symbol and string keys - [@dblock](https://github.com/dblock).
* [#43](https://github.com/ruby-grape/grape-roar/pull/43): Add SimpleCov LCOV reports and a coverage badge; merge Coveralls reports from selected existing ORM matrix entries without duplicate test runs - [@dblock](https://github.com/dblock).
* [#44](https://github.com/ruby-grape/grape-roar/pull/44): Document dynamic Roar rendering options with native Hash responses and verify wrapping, callback data, request context, and request isolation for modules and decorators - [@dblock](https://github.com/dblock).
* [#45](https://github.com/ruby-grape/grape-roar/pull/45): Document native array rendering with Roar's collection factory while preserving collection envelopes and request-aware links - [@dblock](https://github.com/dblock).
* Your contribution here.

### 0.5.0 (2025/09/25)
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Use [Roar](https://github.com/apotonick/roar) with [Grape](https://github.com/in
- [Tell your API to use Grape::Formatter::Roar](#tell-your-api-to-use-grapeformatterroar)
- [Use Grape’s Present](#use-grapes-present)
- [Dynamic Rendering Options](#dynamic-rendering-options)
- [Native Array Representations](#native-array-representations)
- [Hash-backed Representers](#hash-backed-representers)
- [Accessing the Request Inside a Representer](#accessing-the-request-inside-a-representer)
- [Decorators](#decorators)
Expand Down Expand Up @@ -122,6 +123,20 @@ This returns a JSON object wrapped under `"product"`, not a JSON-encoded string.

`present product, with: ProductRepresenter` does not forward arbitrary options to Roar's later rendering step. Use the explicit `to_hash` call above instead, and do not return `to_json` output through the formatter: the formatter would encode that string again. Keep options local to each rendering call rather than modifying shared representer definitions.

### Native Array Representations

To render a plain array using a single-item representer, use Roar's native collection factory explicitly. This works with both representer modules and decorators:

```ruby
get 'products' do
ProductRepresenter.for_collection.prepare(Product.all.to_a).to_hash(user_options: { env: env })
end
```

The response is an array of individually represented products. Empty collections return `[]`, and callbacks receive the request environment. Return the native Array from `to_hash`, not a JSON string, so the formatter encodes it exactly once.

`present products, with: ProductsRepresenter` still treats `ProductsRepresenter` as the representation of the whole collection. This preserves existing collection properties, envelopes, and HAL `_embedded` responses. Automatically treating every Array as a collection of individually represented resources would change those responses; use the explicit factory above when that is what you want.

### Hash-backed Representers

A plain `property :some_property` reads `object.some_property`; it does not read `object[:some_property]`. When presenting a Hash, specify a getter instead of relying on implicit conversion to an object with attribute methods:
Expand Down
79 changes: 79 additions & 0 deletions spec/grape/roar_collections_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

require 'roar/json/hal'

describe Grape::Roar do
include_context 'Grape API App'

shared_examples 'native collection rendering' do
before do
representer.property :id
representer.property :title
representer.link(:self) do |opts|
"#{Grape::Request.new(opts.fetch(:env)).base_url}/products/#{represented.id}"
end
stub_const('CollectionItemRepresenter', representer)

records = [Product.new(id: 1, title: 'First'), Product.new(id: 2, title: 'Second')]
subject.get('/products') do
products = params[:empty] ? [] : records
CollectionItemRepresenter.for_collection.prepare(products).to_hash(user_options: { env: env })
end

wrapper = Module.new do
include Roar::JSON::HAL
include Grape::Roar::Representer
end
wrapper.collection :entries, extend: representer, as: :products, embedded: true
subject.get('/hal') { present records, with: wrapper }
end

it 'renders each item with request-aware links' do
get '/products'

expect(JSON.parse(last_response.body)).to eq(
[
{ 'id' => 1, 'title' => 'First', 'links' => [{ 'rel' => 'self', 'href' => 'http://example.org/products/1' }] },
{ 'id' => 2, 'title' => 'Second', 'links' => [{ 'rel' => 'self', 'href' => 'http://example.org/products/2' }] }
]
)
end

it 'renders an empty collection as an array' do
get '/products', empty: 'true'

expect(JSON.parse(last_response.body)).to eq([])
end

it 'preserves existing collection representers with an embedded envelope' do
get '/hal'

products = JSON.parse(last_response.body).fetch('_embedded').fetch('products')
expect(products.map { |product| product.fetch('title') }).to eq(%w[First Second])
expect(products.first.fetch('links')).to eq([{ 'rel' => 'self', 'href' => 'http://example.org/products/1' }])
end
end

context 'with a module' do
let(:representer) do
Module.new do
include Roar::JSON
include Roar::Hypermedia
include Grape::Roar::Representer
end
end

it_behaves_like 'native collection rendering'
end

context 'with a decorator' do
let(:representer) do
Class.new(Grape::Roar::Decorator) do
include Roar::JSON
include Roar::Hypermedia
end
end

it_behaves_like 'native collection rendering'
end
end
Loading