From f5ceb17d965483ddf21745b3c09675c02d88ec35 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Tue, 27 Oct 2026 06:22:56 -0400 Subject: [PATCH] Document built-in Grape representer lookup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 + README.md | 32 +++++++++++++ spec/grape/roar_lookup_spec.rb | 83 ++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 spec/grape/roar_lookup_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 668144a..25b357a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index 9e87018..b1fa111 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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: diff --git a/spec/grape/roar_lookup_spec.rb b/spec/grape/roar_lookup_spec.rb new file mode 100644 index 0000000..4a2a593 --- /dev/null +++ b/spec/grape/roar_lookup_spec.rb @@ -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