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 @@ -6,6 +6,7 @@
* [#42](https://github.com/ruby-grape/grape-roar/pull/42): Support caching plain rendered representations without forwarding Roar-only options to generic JSON encoders; document cache serialization and request-context keys - [@dblock](https://github.com/dblock).
* [#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).
* Your contribution here.

### 0.5.0 (2025/09/25)
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Use [Roar](https://github.com/apotonick/roar) with [Grape](https://github.com/in
- [Usage](#usage)
- [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)
- [Hash-backed Representers](#hash-backed-representers)
- [Accessing the Request Inside a Representer](#accessing-the-request-inside-a-representer)
- [Decorators](#decorators)
Expand Down Expand Up @@ -103,6 +104,24 @@ get 'products' do
end
```

### Dynamic Rendering Options

To supply Roar rendering options, such as `wrap`, or request-specific callback data, render a native Hash with `to_hash` and return it from the endpoint. Put callback data in `user_options`; include `env` there if any links or properties use the request:

```ruby
get 'product/:id' do
product = Product.find(params[:id])
ProductRepresenter.represent(product).to_hash(
wrap: :product,
user_options: { env: env, student: current_student }
)
end
```

This returns a JSON object wrapped under `"product"`, not a JSON-encoded string. It works with both representer modules and decorators. Hypermedia callbacks receive your user options directly; for example, `link(:student) { |opts| "/students/#{opts.fetch(:student).id}" }` can use the current student.

`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.

### 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
68 changes: 68 additions & 0 deletions spec/grape/roar_render_options_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

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

shared_examples 'rendering with request options' do
before do
representer.property :title
representer.link(:self) { |opts| Grape::Request.new(opts.fetch(:env)).url }
representer.link(:student) { |opts| "/students/#{opts.fetch(:student)}" }
stub_const('RenderingRepresenter', representer)

subject.get('/render') do
product = Product.new(title: 'Lonestar')
RenderingRepresenter.represent(product).to_hash(
wrap: :product,
user_options: { env: env, student: params[:student] }
)
end
end

it 'wraps the object and supplies callback data and the request environment' do
get '/render', student: '123'

expect(JSON.parse(last_response.body)).to eq(
'product' => {
'title' => 'Lonestar',
'links' => [
{ 'rel' => 'self', 'href' => 'http://example.org/render?student=123' },
{ 'rel' => 'student', 'href' => '/students/123' }
]
}
)
end

it 'uses fresh callback options for each request' do
links = %w[123 456].map do |student|
get '/render', student: student
JSON.parse(last_response.body).fetch('product').fetch('links').last.fetch('href')
end

expect(links).to eq(['/students/123', '/students/456'])
end
end

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

it_behaves_like 'rendering with request options'
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 'rendering with request options'
end
end
Loading