From 43ac827e793814bb313faedfb9c1c80368e42bd9 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Mon, 26 Oct 2026 09:35:32 -0400 Subject: [PATCH] Replace deprecated Tilt cache with a synchronized Hash Keep compiled template and missing-layout memoization without Tilt::Cache. Serialize cache population across concurrent requests and retain existing format-specific cache keys. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 37 +-- README.md | 428 ++++++++++++++++-------------- lib/grape-rabl/formatter.rb | 17 +- spec/grape_rabl_formatter_spec.rb | 37 +++ 4 files changed, 292 insertions(+), 227 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47da892..0b7333e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,37 +1,38 @@ -#### Next +#### (Next) * Your contribution here. +* [#55](https://github.com/ruby-grape/grape-rabl/pull/55): Replace deprecated `Tilt::Cache` with a synchronized Hash cache for templates and layouts - [@dblock](https://github.com/dblock). * [#54](https://github.com/ruby-grape/grape-rabl/pull/54): Replaced Travis CI with GitHub Actions, modernized RuboCop and coverage reporting, and added a Ruby 2.7-4.0/Grape 1.x-4.x compatibility matrix; Ruby 2.7 or newer is now required - [@dblock](https://github.com/dblock). * [#54](https://github.com/ruby-grape/grape-rabl/pull/54): Fixed fallback rendering with newer Grape versions, including Grape 4.x - [@dblock](https://github.com/dblock). -#### v0.5.0 +#### 0.5.0 * [#34](https://github.com/ruby-grape/grape-rabl/pulls/34): If no RABL template is specified, fallback to the default response format as determined by Grape - [@chrisbloom7](https://github.com/chrisbloom7). -#### v0.4.3 +#### 0.4.3 * [#44](https://github.com/ruby-grape/grape-rabl/issues/44): Don't require unused hashie - [@tsuwatch](https://github.com/tsuwatch). * [#44](https://github.com/ruby-grape/grape-rabl/issues/44): Support Ruby >= 2.2 - [@tsuwatch](https://github.com/tsuwatch). * [#45](https://github.com/ruby-grape/grape-rabl/pull/45): Added danger, PR linter - [@dblock](https://github.com/dblock). -#### v0.4.2 +#### 0.4.2 * [#43](https://github.com/ruby-grape/grape-rabl/pull/43): Fix template caching for multiple formats - [@kushkella](https://github.com/kushkella). -#### v0.4.1 +#### 0.4.1 * [#39](https://github.com/ruby-grape/grape-rabl/issues/39): Automatically require 'grape/rabl' - [@martinezcoder](https://github.com/martinezcoder). -#### v0.4.0 +#### 0.4.0 * [#37](https://github.com/ruby-grape/grape-rabl/issues/37): Make grape-rabl thread-safe - [@kushkella](https://github.com/kushkella). -#### v0.3.1 +#### 0.3.1 * [#35](https://github.com/ruby-grape/grape-rabl/issues/35): The `render` method will no longer modify endpoint options at runtime - [@yesmeck](https://github.com/yesmeck). -#### v0.3.0 +#### 0.3.0 * [#22](https://github.com/ruby-grape/grape-rabl/pull/22): Enable using a layout template in Rabl - [@koko1000ban](https://github.com/koko1000ban). * Implemented Rubocop, Ruby style linter - [@dblock](https://github.com/dblock). @@ -40,22 +41,22 @@ * Enable support for template caching - [@kushkella](https://github.com/kushkella). -#### v0.2.2 +#### 0.2.2 * [#20](https://github.com/ruby-grape/grape-rabl/pull/20): Relaxed dependency on a specific version of Grape - [@cheef](https://github.com/cheef). -#### v0.2.1 +#### 0.2.1 * [#11](https://github.com/ruby-grape/grape-rabl/pull/11): Fix: render template according to request format - [@alovak](https://github.com/alovak). -#### v0.2.0 +#### 0.2.0 * [#10](https://github.com/ruby-grape/grape-rabl/pull/10): Allow to use partials in Grape - [@ichilton](https://github.com/ichilton). * Stick to gem conventions - [@LTe](https://github.com/lte). * [#13](https://github.com/ruby-grape/grape-rabl/pull/13): Update for Grape 0.3 compatibility - [@alovak](https://github.com/alovak). * Format fix - [@LTe](https://github.com/LTe). -#### v0.1.0 +#### 0.1.0 * Updated w/released Grape 0.2.3 - [@dblock](https://github.com/dblock). * Added link to Rabl - [@dblock](https://github.com/dblock). @@ -63,31 +64,31 @@ * Grape 0.2.3 - [@dblock](https://github.com/dblock). * Updated Grape dependency via .gemspec - [@dblock](https://github.com/dblock). -#### v0.0.6 +#### 0.0.6 * [#6](https://github.com/ruby-grape/grape-rabl/pull/6): Use Grape formatter syntax instead of monkey-patching - [@dblock](https://github.com/dblock). * Close block code in README - [@LTe](https://github.com/LTe). * Change home to user - [@LTe](https://github.com/LTe). -#### v0.0.5 +#### 0.0.5 * Respect `default_format` for rabl response - [@LTe](https://github.com/LTe). -#### v0.0.4 +#### 0.0.4 * Require `grape/rabl` - [@LTe](https://github.com/LTe). -#### v0.0.3 +#### 0.0.3 * Template without `.rabl` - [@LTe](https://github.com/LTe). -#### v0.0.2 +#### 0.0.2 * Add Travis - [@LTe](https://github.com/LTe). * Remove ruby debug - [@LTe](https://github.com/LTe). * Works with rubinius - [@LTe](https://github.com/LTe). * Add dependency status - [@LTe](https://github.com/LTe). -#### v0.0.1 +#### 0.0.1 * Initial public release - [@LTe](https://github.com/lte). diff --git a/README.md b/README.md index 24c2f5e..99000b6 100644 --- a/README.md +++ b/README.md @@ -1,205 +1,223 @@ -# Grape::Rabl - -Use [Rabl](https://github.com/nesquena/rabl) templates in [Grape](https://github.com/ruby-grape/grape)! - -[![Gem Version](http://img.shields.io/gem/v/grape-rabl.svg)](http://badge.fury.io/rb/grape-rabl) -[![Build Status](https://github.com/ruby-grape/grape-rabl/actions/workflows/test.yml/badge.svg)](https://github.com/ruby-grape/grape-rabl/actions/workflows/test.yml) -[![Code Climate](https://codeclimate.com/github/ruby-grape/grape-rabl.svg)](https://codeclimate.com/github/ruby-grape/grape-rabl) -[![Coverage Status](https://img.shields.io/coveralls/ruby-grape/grape-rabl.svg)](https://coveralls.io/r/ruby-grape/grape-rabl?branch=master) - -## Installation - -Requires Ruby 2.7 or newer. CI covers Grape 1.x, 2.x, 3.x, and 4.x; Grape 3.x and 4.x are tested on Ruby 3.3 and newer. Grape 1.x requires Rack 2.x. - -Add the `grape` and `grape-rabl` gems to Gemfile. - -```ruby -gem 'grape' -gem 'grape-rabl' -``` - -And then execute: - - $ bundle - -## Upgrading - -See [UPGRADING](UPGRADING.md). - -## Usage - -### Setup view root directory - -```ruby -# config.ru -use Rack::Config do |env| - env['api.tilt.root'] = '/path/to/view/root/directory' -end -``` - -### Tell your API to use Grape::Formatter::Rabl - -```ruby -class API < Grape::API - format :json - formatter :json, Grape::Formatter::Rabl -end -``` - -### Use rabl templates conditionally - -Add the template name to the API options. - -```ruby -get "/user/:id", :rabl => "user.rabl" do - @user = User.find(params[:id]) -end -``` - -You can use instance variables in the Rabl template. - -```ruby -object @user => :user -attributes :name, :email - -child @project => :project do - attributes :name -end -``` - -### Use rabl layout - -Gape-rabl first looks for a layout file in `#{env['api.tilt.root']}/layouts/application.rabl`. - -You can override the default layout conventions: - -```ruby -# config.ru -use Rack::Config do |env| - env['api.tilt.root'] = '/path/to/view/root/directory' - env['api.tilt.layout'] = 'layouts/another' -end -``` - -### Enable template caching - -Grape-rabl allows for template caching after templates are loaded initially. - -You can enable template caching: - -```ruby -# config.ru -Grape::Rabl.configure do |config| - config.cache_template_loading = true # default: false -end -``` - -## You can omit .rabl - -The following are identical. - -```ruby -get "/home", :rabl => "view" -get "/home", :rabl => "view.rabl" -``` - -### Example - -```ruby -# config.ru -use Rack::Config do |env| - env['api.tilt.root'] = '/path/to/view/root/directory' -end - -class UserAPI < Grape::API - format :json - formatter :json, Grape::Formatter::Rabl - - get '/user/:id' do - @user = User.find(params[:id]) - - # use rabl with 'user.rabl' or 'admin.rabl' template - if @user.admin? - # pass locals with the #render method - render rabl: 'admin', locals: { details: 'this user is an admin' } - else - render rabl: 'user' - end - end - - get '/admin/:id', :rabl => 'admin' do - @user = User.find(params[:id]) - - # use rabl with 'super_admin.rabl' - render rabl: 'super_admin' if @user.super_admin? - # when render method has not been used use template from endpoint definition - end - - # use rabl with 'user_history.rabl' template - get '/user/:id/history', :rabl => 'user_history' do - @history = User.find(params[:id]).history - end - - # do not use rabl, fallback to the default Grape response formatter - get '/users' do - User.all - end -end -``` - -```ruby -# user.rabl -object @user => :user - -attributes :name -``` - -## Usage with rails - -Create grape application - -```ruby -# app/api/user.rb -class MyAPI < Grape::API - format :json - formatter :json, Grape::Formatter::Rabl - get '/user/:id', :rabl => "user" do - @user = User.find(params[:id]) - end -end -``` - -```ruby -# app/views/api/user.rabl -object @user => :user -``` - -Edit your **config/application.rb** and add view path - -```ruby -# application.rb -class Application < Rails::Application - config.middleware.use(Rack::Config) do |env| - env['api.tilt.root'] = Rails.root.join "app", "views", "api" - end -end -``` - -Mount application to rails router - -```ruby -# routes.rb -GrapeExampleRails::Application.routes.draw do - mount MyAPI , :at => "/api" -end -``` - -## Specs - -See ["Writing Tests"](https://github.com/ruby-grape/grape#writing-tests) in [grape](https://github.com/ruby-grape/grape) README. - -Enjoy :) - -## Contributing - -See [CONTRIBUTING](CONTRIBUTING.md). +# Grape::Rabl + +Use [Rabl](https://github.com/nesquena/rabl) templates in [Grape](https://github.com/ruby-grape/grape)! + +[![Gem Version](http://img.shields.io/gem/v/grape-rabl.svg)](http://badge.fury.io/rb/grape-rabl) +[![Build Status](https://github.com/ruby-grape/grape-rabl/actions/workflows/test.yml/badge.svg)](https://github.com/ruby-grape/grape-rabl/actions/workflows/test.yml) +[![Code Climate](https://codeclimate.com/github/ruby-grape/grape-rabl.svg)](https://codeclimate.com/github/ruby-grape/grape-rabl) +[![Coverage Status](https://img.shields.io/coveralls/ruby-grape/grape-rabl.svg)](https://coveralls.io/r/ruby-grape/grape-rabl?branch=master) + +# Table of Contents + +- [Installation](#installation) +- [Upgrading](#upgrading) +- [Usage](#usage) + - [Setup view root directory](#setup-view-root-directory) + - [Tell your API to use Grape::Formatter::Rabl](#tell-your-api-to-use-grapeformatterrabl) + - [Use rabl templates conditionally](#use-rabl-templates-conditionally) + - [Use rabl layout](#use-rabl-layout) + - [Enable template caching](#enable-template-caching) +- [You can omit .rabl](#you-can-omit-rabl) + - [Example](#example) +- [Usage with rails](#usage-with-rails) +- [Specs](#specs) +- [Contributing](#contributing) + +## Installation + +Requires Ruby 2.7 or newer. CI covers Grape 1.x, 2.x, 3.x, and 4.x; Grape 3.x and 4.x are tested on Ruby 3.3 and newer. Grape 1.x requires Rack 2.x. + +Add the `grape` and `grape-rabl` gems to Gemfile. + +```ruby +gem 'grape' +gem 'grape-rabl' +``` + +And then execute: + + $ bundle + +## Upgrading + +See [UPGRADING](UPGRADING.md). + +## Usage + +### Setup view root directory + +```ruby +# config.ru +use Rack::Config do |env| + env['api.tilt.root'] = '/path/to/view/root/directory' +end +``` + +### Tell your API to use Grape::Formatter::Rabl + +```ruby +class API < Grape::API + format :json + formatter :json, Grape::Formatter::Rabl +end +``` + +### Use rabl templates conditionally + +Add the template name to the API options. + +```ruby +get "/user/:id", :rabl => "user.rabl" do + @user = User.find(params[:id]) +end +``` + +You can use instance variables in the Rabl template. + +```ruby +object @user => :user +attributes :name, :email + +child @project => :project do + attributes :name +end +``` + +### Use rabl layout + +Gape-rabl first looks for a layout file in `#{env['api.tilt.root']}/layouts/application.rabl`. + +You can override the default layout conventions: + +```ruby +# config.ru +use Rack::Config do |env| + env['api.tilt.root'] = '/path/to/view/root/directory' + env['api.tilt.layout'] = 'layouts/another' +end +``` + +### Enable template caching + +Grape-rabl allows for template caching after templates are loaded initially. + +Template creation and cache lookups are synchronized across requests, including cached missing layouts. Caching does not depend on the deprecated `Tilt::Cache` class. Disable caching during development if templates need to reload after changes. + +You can enable template caching: + +```ruby +# config.ru +Grape::Rabl.configure do |config| + config.cache_template_loading = true # default: false +end +``` + +## You can omit .rabl + +The following are identical. + +```ruby +get "/home", :rabl => "view" +get "/home", :rabl => "view.rabl" +``` + +### Example + +```ruby +# config.ru +use Rack::Config do |env| + env['api.tilt.root'] = '/path/to/view/root/directory' +end + +class UserAPI < Grape::API + format :json + formatter :json, Grape::Formatter::Rabl + + get '/user/:id' do + @user = User.find(params[:id]) + + # use rabl with 'user.rabl' or 'admin.rabl' template + if @user.admin? + # pass locals with the #render method + render rabl: 'admin', locals: { details: 'this user is an admin' } + else + render rabl: 'user' + end + end + + get '/admin/:id', :rabl => 'admin' do + @user = User.find(params[:id]) + + # use rabl with 'super_admin.rabl' + render rabl: 'super_admin' if @user.super_admin? + # when render method has not been used use template from endpoint definition + end + + # use rabl with 'user_history.rabl' template + get '/user/:id/history', :rabl => 'user_history' do + @history = User.find(params[:id]).history + end + + # do not use rabl, fallback to the default Grape response formatter + get '/users' do + User.all + end +end +``` + +```ruby +# user.rabl +object @user => :user + +attributes :name +``` + +## Usage with rails + +Create grape application + +```ruby +# app/api/user.rb +class MyAPI < Grape::API + format :json + formatter :json, Grape::Formatter::Rabl + get '/user/:id', :rabl => "user" do + @user = User.find(params[:id]) + end +end +``` + +```ruby +# app/views/api/user.rabl +object @user => :user +``` + +Edit your **config/application.rb** and add view path + +```ruby +# application.rb +class Application < Rails::Application + config.middleware.use(Rack::Config) do |env| + env['api.tilt.root'] = Rails.root.join "app", "views", "api" + end +end +``` + +Mount application to rails router + +```ruby +# routes.rb +GrapeExampleRails::Application.routes.draw do + mount MyAPI , :at => "/api" +end +``` + +## Specs + +See ["Writing Tests"](https://github.com/ruby-grape/grape#writing-tests) in [grape](https://github.com/ruby-grape/grape) README. + +Enjoy :) + +## Contributing + +See [CONTRIBUTING](CONTRIBUTING.md). diff --git a/lib/grape-rabl/formatter.rb b/lib/grape-rabl/formatter.rb index 1aba1ca..cafcbd9 100644 --- a/lib/grape-rabl/formatter.rb +++ b/lib/grape-rabl/formatter.rb @@ -5,9 +5,16 @@ module Grape module Rabl class Formatter + @tilt_cache = {} + @tilt_cache_mutex = Mutex.new + class << self - def tilt_cache - @tilt_cache ||= ::Tilt::Cache.new + attr_reader :tilt_cache + + def fetch_cached_template(key) + @tilt_cache_mutex.synchronize do + tilt_cache.fetch(key) { tilt_cache[key] = yield } + end end end @@ -76,7 +83,9 @@ def set_view_root def tilt_template(template) if Grape::Rabl.configuration.cache_template_loading - Grape::Rabl::Formatter.tilt_cache.fetch(tilt_cache_key(template)) { ::Tilt.new(view_path(template), tilt_options) } + Grape::Rabl::Formatter.fetch_cached_template(tilt_cache_key(template)) do + ::Tilt.new(view_path(template), tilt_options) + end else ::Tilt.new(view_path(template), tilt_options) end @@ -89,7 +98,7 @@ def tilt_options def layout_template layout_path = view_path(env['api.tilt.layout'] || 'layouts/application') if Grape::Rabl.configuration.cache_template_loading - Grape::Rabl::Formatter.tilt_cache.fetch(tilt_cache_key(layout_path)) do + Grape::Rabl::Formatter.fetch_cached_template(tilt_cache_key(layout_path)) do ::Tilt.new(layout_path, tilt_options) if File.exist?(layout_path) end else diff --git a/spec/grape_rabl_formatter_spec.rb b/spec/grape_rabl_formatter_spec.rb index f40884b..8a95f2c 100644 --- a/spec/grape_rabl_formatter_spec.rb +++ b/spec/grape_rabl_formatter_spec.rb @@ -2,6 +2,43 @@ require 'spec_helper' +describe Grape::Rabl::Formatter do + before do + hide_const('Tilt::Cache') + described_class.tilt_cache.clear + end + + after do + described_class.tilt_cache.clear + end + + it 'reuses a cached template without Tilt::Cache' do + template = described_class.fetch_cached_template(:template) { Object.new } + + expect(described_class.fetch_cached_template(:template) { raise 'recompiled' }) + .to equal(template) + end + + it 'memoizes missing layouts' do + expect(described_class.fetch_cached_template(:missing) { nil }).to be_nil + expect(described_class.fetch_cached_template(:missing) { raise 'looked up again' }) + .to be_nil + end + + it 'creates one cached template for concurrent requests' do + threads = 4.times.map do + Thread.new do + described_class.fetch_cached_template(:concurrent) do + sleep 0.01 + Object.new + end + end + end + + expect(threads.map(&:value).map(&:object_id).uniq.size).to eq(1) + end +end + describe 'Grape::Rabl formatter' do subject do Class.new(Grape::API)