diff --git a/CHANGELOG.md b/CHANGELOG.md index 86e43bd..7b2cfc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index 75cc131..10e7349 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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: diff --git a/spec/grape/roar_render_options_spec.rb b/spec/grape/roar_render_options_spec.rb new file mode 100644 index 0000000..ddf41b2 --- /dev/null +++ b/spec/grape/roar_render_options_spec.rb @@ -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