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 @@ -8,6 +8,7 @@
* [#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).
* [#46](https://github.com/ruby-grape/grape-roar/pull/46): Document Grape's built-in representer lookup through model Entity constants and API-local decorator mappings, including explicit overrides - [@dblock](https://github.com/dblock).
* Your contribution here.

### 0.5.0 (2025/09/25)
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Use [Roar](https://github.com/apotonick/roar) with [Grape](https://github.com/in
- [Use Grape’s Present](#use-grapes-present)
- [Dynamic Rendering Options](#dynamic-rendering-options)
- [Native Array Representations](#native-array-representations)
- [Representer Lookup](#representer-lookup)
- [Hash-backed Representers](#hash-backed-representers)
- [Accessing the Request Inside a Representer](#accessing-the-request-inside-a-representer)
- [Decorators](#decorators)
Expand Down Expand Up @@ -137,6 +138,37 @@ The response is an array of individually represented products. Empty collections

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

### Representer Lookup

Grape can infer a representer from a model's `Entity` constant. Set it to a representer module or decorator, then call `present` without `with:`:

```ruby
class Product
Entity = ProductRepresenter
end

get 'product/:id' do
present Product.find(params[:id])
end
```

For decorator classes, an API-local mapping avoids adding a constant to the model:

```ruby
class API < Grape::API
format :json
formatter :json, Grape::Formatter::Roar

represent Product, with: ProductRepresenter

get 'product/:id' do
present Product.find(params[:id])
end
end
```

An explicit `present object, with: OtherRepresenter` overrides inference. These are Grape's existing lookup mechanisms; grape-roar does not guess global constants from model names, choose between singular and plural representers, or automatically represent arbitrary endpoint return values. Call `present` to request inference.

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

describe Grape::Roar do
subject(:api) { Class.new(Grape::API) }

before do
api.format :json
api.formatter :json, Grape::Formatter::Roar
end

def app
api
end

shared_examples 'inferred model representations' do
before do
stub_const('LookupProduct', Class.new(Product))
representer.property :id
representer.property :title
stub_const('LookupProduct::Entity', representer)

api.get('/product') { present LookupProduct.new(id: 1, title: 'Lonestar') }

override = Module.new do
include Roar::JSON
include Grape::Roar::Representer

property :id
end
api.get('/explicit') { present LookupProduct.new(id: 1, title: 'Lonestar'), with: override }
end

it 'finds the model Entity without a with option' do
get '/product'

expect(JSON.parse(last_response.body)).to eq('id' => 1, 'title' => 'Lonestar')
end

it 'lets an explicit representer override inference' do
get '/explicit'

expect(JSON.parse(last_response.body)).to eq('id' => 1)
end
end

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

it_behaves_like 'inferred model representations'
end

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

it_behaves_like 'inferred model representations'
end

context 'with an API-local mapping' do
before do
decorator = Class.new(Grape::Roar::Decorator) do
include Roar::JSON

property :title
end
api.represent Product, with: decorator
api.get('/product') { present Product.new(title: 'Lonestar', id: 1) }
end

it 'uses the registered decorator without modifying the model' do
get '/product'

expect(JSON.parse(last_response.body)).to eq('title' => 'Lonestar')
expect(Product.const_defined?(:Entity, false)).to be false
end
end
end
Loading