rabl 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14b2f3bca3110dd76fbfa8c05c2dc068ed41719a1ab7c3812450e82869681ed2
4
- data.tar.gz: 925dbbbda7e8557f4bafd1d41004783239ba75f1ee0f630debd821a3b9fa3919
3
+ metadata.gz: f1bfaeaa40af7667acd5ed068e1bd0b54dd5f201ec360bb78dd51fcb42198343
4
+ data.tar.gz: 5389a879fe511adcf009b51edbf47c77042374cac7582cd287a699ca7560caa8
5
5
  SHA512:
6
- metadata.gz: d41aacd3750c05a910b0dfeff320b1b3953c7569cfa8326f384e770a81fdb98ef1bd6dc7a57510e4cc31377e4f26cf52c3efbf97b7093f505057813226288a50
7
- data.tar.gz: 1026b5def5c394e27dac2fcf15852045ea5d650a37c7734bc1b50d2f4bc83cb89bbe3aa67791b24cf0938841813af95215441b9e11085dcc1352dce121e8b16e
6
+ metadata.gz: 1071859094149f84a0d9127a9acd99101537278694390bab3d1a2eb1d5b59ac50466e222e7117a627df114fc5c2662eec9b7ea555bbb7b975737d9f979049c3e
7
+ data.tar.gz: 248bdcc28e051445cf08d4d8c6f5f5911ecb83cc453844514cf5460d240b522b12257e41a4ef82b3b2f47787079142ec56a0626e227306931ebd42764e1b7c09
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.19.1 (Nov 5, 2026)
4
+
5
+ ### Other changes
6
+
7
+ * With `camelize_keys` on, a key is camelized once and remembered rather than for each item of a collection: a collection hands the same names out for every item, and camelizing one takes about 0.5 µs and six objects for a one-word name and about twenty for one with three words. Measured on a 50-item collection whose template has three `attributes`, one `node` and one `child`, six key names per item, YJIT off: with `camelize_keys = :lower` the render went from about 582 µs and 4050 allocations to about 457 µs and 2000 on Ruby 3.3.12, about 1.3x, and from about 592 µs and 4054 allocations to about 464 µs and 2004 on Ruby 4.0.7, where the same render without `camelize_keys` takes about 444 µs and 2000 on 3.3.12 and 453 µs and 2004 on 4.0.7 and is unchanged, so a template with more keys per item gains more. The names are kept in two maps, one per case style, so each style keeps its own; a map is emptied when it holds 10,000 names and a name longer than 128 bytes is camelized each time rather than kept, so a template that names keys at runtime (`node(field.key)`, `attribute :as => "field_#{i}"`, a name taken from user input) cannot grow it without bound. A name is kept as a frozen copy, because a Hash keeps an unfrozen String subclass such as `ActiveSupport::SafeBuffer` as it is and a caller that changed such a name in place would file another name under the camelized value; and a name keeps the acronyms it was first camelized with, so an inflection added after a name was first seen doesn't reach that name. The output is otherwise unchanged (@tagliala; change written by DeepSeek Flash)
8
+
9
+ * A template a collection uses for each item through `extends` or `partial` is now read and resolved once per render rather than once per item. With the default `cache_sources` off, every item ran the whole lookup itself: outside Rails, with a view path passed and one in `Rabl.configuration.view_paths`, which `Rabl::Renderer`'s lookup adds to it, one lookup costs about 117 µs and 62 allocations on Ruby 4.0.7 and about 110 µs on 3.3.12, YJIT off, against 1.8 µs and 4 with `cache_sources` on. The sources a render reads are now collected for the length of that render and dropped when it returns, so a template changed between renders is still read again, one changed while the render runs is not read again until the next render, and a render that raises leaves nothing behind. A render started inside the collection (a `Rabl::Renderer` in a `node`, a nested render under Rails) uses the collection it finds, whose keys are complete, so the render around it keeps its own sources. Outside Rails the collection keys a lookup by the values it depends on (the format, the locale, the default locale and the object its fallbacks come from, the app's views under Sinatra) and not by the numbers `LOOKUP_IDS` and `MANUAL_LOOKUP_IDS` keep for the life of the process, so the collection's keys add nothing to those tables nor to `I18n.fallbacks`. With `cache_sources` on or off, a template with more than one candidate file for a name still asks I18n for the locale's fallbacks when it ranks them, which keeps that locale in `I18n.fallbacks`, as it did before; with `cache_sources` on the tables and `I18n.fallbacks` also gain an entry per locale looked up in, as before. Under Rails the numbers come from the lookup details and view paths Rails itself keeps, so they are used as they are and a view path prepended for one request adds one, as it does to Rails; a Padrino lookup, which depends on the request's controller and content type, is not collected per render. Measured outside Rails on a 50-item collection whose template is three `attributes`, one `node` and one `child` plus `extends "posts/show"` (the same output as one template with all of them), the render went from about 5.8 ms and 7540 allocations to about 0.95 ms and 4456 on Ruby 3.3.12, about 6.2x, and from about 7.0 ms and 7244 allocations to about 1.0 ms and 4454 on Ruby 4.0.7, about 7.0x; a heavier template, with two more `node`s, a `child`, a `glue` and a `partial` per item, went from about 12.1 ms and 16649 allocations to about 2.2 ms and 10480 on 3.3.12, about 5.4x, and from about 13.8 ms and 16103 to about 2.3 ms and 10522 on 4.0.7, about 5.9x, so the gain follows how much of the per-item cost the lookup was. Under Rails 8.1.4 on Ruby 4.0.7, rendering the same collection through `ApplicationController.render` with `cache_sources` off went from about 2.7 ms and 6376 allocations to about 1.9 ms and 5203, about 1.4x, where the same render with `cache_sources` on takes about 1.8 ms either way: Rails repeats a lookup in about 17 µs per item, the difference between that render with `cache_sources` on and off on the same code, against about 117 µs for the manual lookup, so it has less to save. The one-template version of the output is unchanged within the noise, one allocation more (the render's own collection), and a single object using `extends` allocates about ten more objects than before (the render's own collection, and a frozen copy of each lookup's template name and view paths). The output is unchanged (@tagliala; change written by DeepSeek Flash)
10
+
11
+ * `Rabl::Builder#cache_results` and `Rabl::Engine#cache_results` now ask their cheapest question first: whether `cache_all_output` is on, and whether the template set a cache key, before asking whether caching is configured. Under Rails that second question is `ActionController::Base.perform_caching`, a delegated call that allocates an object and takes about 250 ns on Ruby 4.0.7, Rails 8.1.4, YJIT off; with the defaults (`cache_all_output` off, no `cache` in the template) its answer could not change the guard, and it was computed once per object built and once per engine rendered. On Ruby 4.0.7, where the call allocates one object, a 50-item collection whose template has one `child` (100 builders, 51 engines) saves those 151 allocations and about 40 µs per render, about 1.08x; on Ruby 3.3 it allocates two, so the same collection saves 302. A single object saves 4 allocations and about 1 µs. The output is unchanged; with `cache_all_output` on, the builder runs the same checks in the cheap-first order, and an engine whose template sets no cache key still skips the call. Outside Rails both checks are plain configuration reads, so the difference is inside the noise and the allocation count is unchanged (@tagliala)
12
+
13
+ ## 0.19.0 (Nov 1, 2026)
14
+
15
+ ### Requirements
16
+
17
+ * Require ActiveSupport 6.1 or newer, and so Rails 6.1 or newer under Rails. The code rabl kept for Rails 2 to 5 (their template handlers, digestors and template lookups) is removed; Rails 6.1 and newer, Sinatra and Padrino render as before. Apps on an older Rails or ActiveSupport can stay on 0.18.x (@nesquena)
18
+
19
+ ### Fixes
20
+
21
+ * Load rabl's Rails integration on Rails 9 and later: rabl recognised Rails 3 to 8 by the first digit of `Rails.version`, so on a newer Rails the railtie and the `.rabl` template handler never loaded and actions rendered empty responses. rabl no longer checks the Rails version, so its integration loads on every Rails from 6.1 on, with no upper bound; Rails 6.1 to 8 load the same parts as before (@nesquena)
22
+ * Fix rabl defining a top-level `LRU` class for its own cache, which raised `TypeError: superclass mismatch for class LRU` in apps with an `LRU` class of their own; it is now `Rabl::CacheEngine::LRU` (use that name if your code used rabl's `LRU`). Once that cache (used outside Rails) held its 100,000 entries, each write also copied every key to drop the oldest one, about 0.5 ms per write; it now drops the same entries in a few µs (@nesquena)
23
+ * Fix `Rabl::Renderer` and `Rabl.render` raising `NoMethodError` (`undefined method 'underscore' for nil`) for an object, or a collection, whose class has no name, such as a `Struct.new` or `Class.new` object. The renderer sets an instance variable named after the object's class (`@post` for a `Post`); for an unnamed class it now sets none, and the template gets the object as for any other (@nesquena)
24
+
25
+ ### Other changes
26
+
27
+ * `Rabl::Renderer#process_source` and the JSONP callback check in `Rabl::Engine` answer their yes/no question about a string with `String#include?` and `Regexp#match?` instead of `=~`, which built a `MatchData` neither check read: creating a renderer for a template string is about 1.2x faster and allocates 3 objects fewer, a full render about 2% faster and 3 objects fewer, and a render with `enable_json_callbacks` 3 objects fewer. The output is unchanged, and a source that is not valid in its encoding still raises the same `ArgumentError: invalid byte sequence in UTF-8`, from `Engine#eval_source` when it has a newline and from the template lookup when it doesn't; a source in a non-ASCII-compatible encoding, such as UTF-16LE, still raises `Encoding::CompatibilityError` from the check, now with Ruby's `incompatible character encodings` message rather than `incompatible encoding regexp match` (@tagliala; change written by DeepSeek Flash, reviewed by Claude Opus 5.5 and Claude Fable 5.1)
28
+ * CI now generates fresh Rails 7.2, Rails 8.1 and Sinatra apps that use rabl from the checkout and runs their tests (`test/apps/build_and_test.sh`), in place of the checked-in fixture apps for Rails 2 to 6 and Padrino under `fixtures/`, which CI never ran. No change to the gem (@nesquena)
29
+ * README: a `child` block is passed the child's data (e.g. the posts) and a `glue` block the glued object, not the parent, with examples that run; the partial example passes the post (`:object => post`, not `:post`); the JSON engine is Oj when it is loaded and Ruby's JSON otherwise (yajl isn't used); and under Rails, Rails sets the content type from the template's format. No behaviour change (@nesquena; reported by @nirvdrum in #393 and @adie in #580)
30
+ * The gem's rubygems.org page links the changelog, source code and issue tracker, and pushes to rubygems.org now require MFA (@nesquena)
31
+
3
32
  ## 0.18.0 (Oct 31, 2026)
4
33
 
5
34
  ### Requirements
data/README.md CHANGED
@@ -26,6 +26,9 @@ For a breakdown of common misconceptions about RABL, please check out our guide
26
26
 
27
27
  ## Breaking Changes ##
28
28
 
29
+ * v0.19.0 requires ActiveSupport 6.1 or newer (and so Rails 6.1 or newer under Rails). Apps on an older Rails or
30
+ ActiveSupport can stay on 0.18.x.
31
+
29
32
  * v0.18.0 requires Ruby 2.7 or newer (older Rubies can stay on 0.17.0). Every cache key changes once: expect a cold
30
33
  cache after upgrading. It also fixes several long-standing output bugs in ways that can change existing output. Check the
31
34
  [CHANGELOG](https://github.com/nesquena/rabl/blob/master/CHANGELOG.md) before upgrading.
@@ -55,12 +58,12 @@ or add to your Gemfile:
55
58
  ```ruby
56
59
  # Gemfile
57
60
  gem 'rabl'
58
- # Also add either `oj` or `yajl-ruby` as the JSON parser
61
+ # Optionally add `oj`: RABL encodes JSON with Oj when it is loaded, and with Ruby's JSON library otherwise
59
62
  # If using `oj`, Rabl renders with Oj's :compat mode and sets it as Oj's default (with Oj 3.17.7+, only if you haven't configured Oj yet)
60
63
  gem 'oj'
61
64
  ```
62
65
 
63
- and run `bundle install` to install the dependency. RABL needs Ruby 2.7 or newer.
66
+ and run `bundle install` to install the dependency. RABL needs Ruby 2.7 or newer and ActiveSupport 6.1 or newer (on Rails, Rails 6.1 or newer).
64
67
 
65
68
  To render XML, also add `gem 'builder'` (Rails apps already include it); RABL loads ActiveSupport's XML conversions itself.
66
69
 
@@ -135,7 +138,7 @@ Rabl.configure do |config|
135
138
  # config.cache_engine = Rabl::CacheEngine.new # Defaults to Rails cache
136
139
  # config.perform_caching = false
137
140
  # config.escape_all_output = false
138
- # config.json_engine = nil # Class with #dump class method (defaults JSON)
141
+ # config.json_engine = nil # Class with #dump class method (defaults to Oj when it is loaded, else ::JSON)
139
142
  # config.msgpack_engine = nil # Defaults to ::MessagePack
140
143
  # config.bson_engine = nil # Defaults to ::BSON
141
144
  # config.plist_engine = nil # Defaults to ::Plist::Emit
@@ -369,15 +372,18 @@ child :posts => :foobar do
369
372
  end
370
373
  ```
371
374
 
372
- You can also pass in the current object:
375
+ The block is passed the child's data (here the user's posts, not the user), and the parent is still
376
+ available as `@user`:
373
377
 
374
378
  ```ruby
375
379
  object @user
376
- child :posts do |user|
377
- attribute :title unless user.suspended?
380
+ child :posts do |posts|
381
+ attribute :title unless @user.suspended?
378
382
  end
379
383
  ```
380
384
 
385
+ To decide for each post, use a condition, which is passed the post: `attribute :title, :if => lambda { |post| post.published? }`.
386
+
381
387
  ### Gluing Attributes ###
382
388
 
383
389
  You can also append child attributes back to the root node:
@@ -391,11 +397,11 @@ end
391
397
 
392
398
  Use glue to add additional attributes to the parent object.
393
399
 
394
- You can also pass in the current object:
400
+ The block is passed the glued object (here `@post`, not the user):
395
401
 
396
402
  ```ruby
397
403
  object @user
398
- glue(@post) {|user| attribute :title if user.active? }
404
+ glue(@post) { |post| attribute :title if post.published? }
399
405
  ```
400
406
 
401
407
  ### Custom Nodes ###
@@ -481,7 +487,7 @@ collection @posts
481
487
 
482
488
  extends('posts/show', :locals => { :hide_comments => true })
483
489
  # or using partial instead of extends
484
- # node(false) { |post| partial('posts/show', :object => :post, :locals => { :hide_comments => true })}
490
+ # node(false) { |post| partial('posts/show', :object => post, :locals => { :hide_comments => true })}
485
491
  ```
486
492
 
487
493
  and then access locals in the sub-template:
@@ -622,7 +628,7 @@ entry per viewer, apart from the nil case above. Include `root_object` whenever
622
628
  Under Rails, keys include the Rails template digest when the template is rendered through a controller or view. `Rabl.render` and
623
629
  `Rabl::Renderer` used outside a request have no view context, so their keys identify the template differently and don't match the keys of
624
630
  a request. To render outside a request with the same keys, for example to warm the cache in a background job, render through the
625
- controller (Rails 5 and newer):
631
+ controller:
626
632
 
627
633
  ```ruby
628
634
  PostsController.render(:show, assigns: { post: post }, formats: [:json])
@@ -673,13 +679,14 @@ node(:title) { locals[:custom_title] }
673
679
 
674
680
  ### Content Type Headers ###
675
681
 
676
- Currently in RABL, the content-type of your response is not set automatically. This is because RABL is intended
677
- to work for any Rack-based framework and as agnostic to format as possible.
678
- Check [this issue](https://github.com/nesquena/rabl/issues/185#issuecomment-4501232) for more
679
- details, and if you have any ideas or patches please let me know.
682
+ Under Rails, Rails sets the content type from the template's format: `show.json.rabl` is served as
683
+ `application/json` and `show.xml.rabl` as `application/xml`. A template without a format in its name
684
+ (`show.rabl`) is served as `application/json` whatever it renders, so name templates by format when you
685
+ serve more than JSON.
680
686
 
681
- In the meantime, be sure to set the proper content-types if needed. This is usually pretty simple in both
682
- Rails and Padrino. I recommend a before_filter on that controller or directly specified in an action.
687
+ Outside Rails, RABL doesn't set the content type, as it is intended to work for any Rack-based framework and
688
+ to be as agnostic to format as possible (see [this issue](https://github.com/nesquena/rabl/issues/185#issuecomment-4501232)).
689
+ Set it in your app, e.g. with Sinatra's `content_type :json` in the route or a `before` filter.
683
690
 
684
691
  ## Resources ##
685
692
 
@@ -741,6 +748,8 @@ Check out the [Issues](https://github.com/nesquena/rabl/issues) tab for a full l
741
748
 
742
749
  * Rigorous benchmarking and performance optimizations
743
750
 
751
+ To report a security vulnerability, don't open an issue; see [SECURITY.md](https://github.com/nesquena/rabl/blob/master/SECURITY.md).
752
+
744
753
  ## Authors and Contributors ##
745
754
 
746
755
  Active Maintainers:
data/SECURITY.md ADDED
@@ -0,0 +1,14 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes go into the latest release line of rabl. Please check that the problem occurs on the latest release.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Please report vulnerabilities privately, through GitHub's private vulnerability reporting: on the repository's
10
+ [Security tab](https://github.com/nesquena/rabl/security), choose "Report a vulnerability". Don't open a public issue or
11
+ pull request for a vulnerability.
12
+
13
+ Include the rabl version, the steps to reproduce the problem and its impact. The discussion continues in the report
14
+ until a fix is released.
data/lib/rabl/builder.rb CHANGED
@@ -256,7 +256,8 @@ module Rabl
256
256
  # Caches the results of the block based on object cache_key
257
257
  # cache_results { compile_hash(options) }
258
258
  def cache_results(&block)
259
- if template_cache_configured? && Rabl.configuration.cache_all_output && @_object.respond_to?(:cache_key)
259
+ # cheap questions first: template_cache_configured? allocates under Rails
260
+ if Rabl.configuration.cache_all_output && template_cache_configured? && @_object.respond_to?(:cache_key)
260
261
  cache_key = [@_object, @options[:root_name], @options[:format]]
261
262
  engine = rabl_template_engine
262
263
  if engine
@@ -273,11 +274,9 @@ module Rabl
273
274
  end
274
275
 
275
276
  def create_key(name)
276
- if Rabl.configuration.camelize_keys
277
- name.to_s.camelize(Rabl.configuration.camelize_keys == :upper ? :upper : :lower).to_sym
278
- else
279
- name.to_sym
280
- end
277
+ config = Rabl.configuration
278
+ style = config.camelize_keys
279
+ style ? config.camelized_key(name, style == :upper) : name.to_sym
281
280
  end
282
281
  end
283
282
  end
@@ -7,29 +7,28 @@
7
7
  # config.cache_engine = AdvancedCacheEngine.new
8
8
  #
9
9
 
10
- class LRU < Hash
11
- attr_accessor :max_size
10
+ module Rabl
11
+ class CacheEngine
12
+ # A Hash holding at most max_size entries, which drops the entries added first
13
+ class LRU < Hash
14
+ attr_accessor :max_size
12
15
 
13
- def initialize
14
- super
15
- self.max_size = 100_000
16
- end
16
+ def initialize
17
+ super
18
+ self.max_size = 100_000
19
+ end
17
20
 
18
- def []= k,v
19
- r = super
20
- limit_size
21
- r
22
- end
21
+ def []= k,v
22
+ r = super
23
+ limit_size
24
+ r
25
+ end
23
26
 
24
- def limit_size
25
- if size > max_size then
26
- delete keys.shift while size > max_size
27
+ def limit_size
28
+ shift while size > max_size
29
+ end
27
30
  end
28
- end
29
- end
30
31
 
31
- module Rabl
32
- class CacheEngine
33
32
  def initialize
34
33
  unless defined?(Rails)
35
34
  @cache = LRU.new
@@ -122,6 +122,50 @@ module Rabl
122
122
  @exclude_nil_values = false
123
123
  @exclude_empty_values_in_collections = false
124
124
  @camelize_keys = false
125
+ @camelized_keys = { false => {}, true => {} }
126
+ end
127
+
128
+ # Names camelized in each style, by whether the first letter is upper case: a collection
129
+ # builds the same keys for each of its items, and camelizing one takes about 0.5 µs.
130
+ # ceiling: one entry per distinct key name, so a template that names keys at runtime
131
+ # (node(field.key)) keeps them for the life of the process until a new name arrives and the
132
+ # map is already full, when it is emptied; a name over CAMELIZED_KEY_LENGTH_LIMIT bytes isn't
133
+ # kept at all; and a name keeps the acronyms it was first camelized with, so an inflection
134
+ # added later doesn't reach a name that was already seen.
135
+ CAMELIZED_KEYS_LIMIT = 10_000
136
+ # A name longer than this, in bytes, is camelized each time instead of being kept, so a map
137
+ # holds at most CAMELIZED_KEYS_LIMIT names of up to this many bytes each
138
+ CAMELIZED_KEY_LENGTH_LIMIT = 128
139
+
140
+ # Files a new name in a style's map: the maps are shared by every thread
141
+ CAMELIZED_KEYS_LOCK = Mutex.new
142
+
143
+ # Returns name camelized in the given style, camelizing each kept name once
144
+ def camelized_key(name, upper = false)
145
+ keys = @camelized_keys[upper]
146
+ cached = keys[name]
147
+ return cached if cached
148
+
149
+ string = name.to_s
150
+ style = upper ? :upper : :lower
151
+ return string.camelize(style).to_sym if string.bytesize > CAMELIZED_KEY_LENGTH_LIMIT
152
+
153
+ # a Hash freezes a plain String key and copies it, but keeps an unfrozen String subclass,
154
+ # such as ActiveSupport::SafeBuffer, as it is: a caller that changes the name in place would
155
+ # file another name under the camelized value, so the key is copied and frozen here
156
+ # the map is shared by every thread, so a new name is filed under the lock, as in LOOKUP_IDS
157
+ key = Rabl.frozen_copy(name)
158
+ CAMELIZED_KEYS_LOCK.synchronize do
159
+ keys.fetch(key) do
160
+ keys.clear if keys.size >= camelized_keys_limit
161
+ keys[key] = string.camelize(style).to_sym
162
+ end
163
+ end
164
+ end
165
+
166
+ # How many names a style's map keeps before a new name empties it
167
+ def camelized_keys_limit
168
+ CAMELIZED_KEYS_LIMIT
125
169
  end
126
170
 
127
171
  # @return The JSON engine used to encode Rabl templates into JSON
data/lib/rabl/digestor.rb CHANGED
@@ -3,28 +3,8 @@
3
3
  require 'action_view'
4
4
 
5
5
  module Rabl
6
+ # Rails' digestor, under rabl's name: rabl.rb defines it only with the rest of the Rails integration, so
7
+ # Engine#digestor_available? checks for it before adding a template digest to a cache key
6
8
  class Digestor < ActionView::Digestor
7
- # Override the original digest function to ignore partial which
8
- # rabl doesn't use the Rails conventional _ symbol.
9
- if Gem::Version.new(Rails.version) >= Gem::Version.new('5.0.0.beta1')
10
- require 'rabl/digestor/rails5'
11
- elsif Gem::Version.new(Rails.version) >= Gem::Version.new('4.1')
12
- require 'rabl/digestor/rails41'
13
- else
14
- require 'rabl/digestor/rails3'
15
- end
16
-
17
- private
18
- def dependency_digest
19
- template_digests = (dependencies - [template.virtual_path]).collect do |template_name|
20
- if Gem::Version.new(Rails.version) >= Gem::Version.new('4.1')
21
- Digestor.digest(:name => template_name, :finder => finder)
22
- else
23
- Digestor.digest(template_name, format, finder)
24
- end
25
- end
26
-
27
- (template_digests + injected_dependencies).join("-")
28
- end
29
9
  end
30
10
  end
data/lib/rabl/engine.rb CHANGED
@@ -60,9 +60,7 @@ module Rabl
60
60
  # count when no rule matches.
61
61
  def singular_rule_rank(word)
62
62
  inflections = ActiveSupport::Inflector.inflections
63
- uncountables = inflections.uncountables
64
- # ActiveSupport < 5 keeps them in a plain Array, checked by the last word
65
- return -1 if uncountables.respond_to?(:uncountable?) ? uncountables.uncountable?(word) : uncountables.include?(word.downcase[/\b\w+\Z/])
63
+ return -1 if inflections.uncountables.uncountable?(word)
66
64
 
67
65
  rules = inflections.singulars
68
66
  rules.index { |(rule, _)| rule.is_a?(Regexp) ? rule.match?(word) : word.include?(rule) } || rules.size
@@ -149,16 +147,7 @@ module Rabl
149
147
  if digestor_available? && respond_to?(:lookup_context) && lookup_context
150
148
  template = @_options[:template] || @virtual_path
151
149
 
152
- digest =
153
- if Rails.version.to_s =~ /^[678]/
154
- Digestor.digest(name: template, finder: lookup_context, format: :rabl)
155
- elsif Gem::Version.new(Rails.version) >= Gem::Version.new('4.1')
156
- Digestor.digest(:name => template, :finder => lookup_context)
157
- else
158
- Digestor.digest(template, :rabl, lookup_context)
159
- end
160
-
161
- cache_key << digest
150
+ cache_key << Digestor.digest(name: template, finder: lookup_context, format: :rabl)
162
151
  # The digest covers the template source, but not the block that defines this engine
163
152
  if @_block_identity
164
153
  block, parent = @_block_identity
@@ -187,6 +176,14 @@ module Rabl
187
176
  options[:root_name] = determine_object_root(data, root_name, options[:root])
188
177
  options[:template_engine] = self
189
178
 
179
+ # A template the collection uses for each item (extends, partial) is read once per render,
180
+ # not once per item. The render that starts a collection ends it, so a render started inside
181
+ # this one (a Rabl::Renderer in a node, a nested render under Rails) uses the collection it
182
+ # finds, whose keys are complete, and the render around it keeps its own sources. A nested
183
+ # engine renders inside a collection anyway, so it skips the look for one.
184
+ owns_source_cache = !@_options[:nested] && Rabl.render_source_cache.nil?
185
+ Rabl.begin_render_source_cache if owns_source_cache
186
+
190
187
  @_rendering = true
191
188
  result =
192
189
  if is_object?(data) || !data # object @user
@@ -201,6 +198,7 @@ module Rabl
201
198
  result
202
199
  ensure
203
200
  @_rendering = false
201
+ Rabl.end_render_source_cache if owns_source_cache
204
202
  end
205
203
 
206
204
  def to_dumpable(options = {})
@@ -594,7 +592,7 @@ module Rabl
594
592
 
595
593
  callback = request_params[:callback]
596
594
  return unless callback.is_a?(String) && callback.length <= RABL_JSON_CALLBACK_MAX_LENGTH
597
- return unless callback.ascii_only? && callback =~ RABL_JSON_CALLBACK_NAME
595
+ return unless callback.ascii_only? && callback.match?(RABL_JSON_CALLBACK_NAME)
598
596
 
599
597
  callback
600
598
  end
@@ -638,7 +636,8 @@ module Rabl
638
636
  # Caches the results of the block based on object cache_key
639
637
  # cache_results { compile_hash(options) }
640
638
  def cache_results(&block)
641
- return yield unless template_cache_configured? && defined?(@_cache_key)
639
+ # cheap question first: template_cache_configured? allocates under Rails
640
+ return yield unless defined?(@_cache_key) && template_cache_configured?
642
641
 
643
642
  if cache_read_on_render?
644
643
  fetch_result_from_cache(cache_key, @_cache_options, &block)
@@ -647,8 +646,9 @@ module Rabl
647
646
  end
648
647
  end
649
648
 
649
+ # rabl.rb loads Rabl::Digestor with the rest of the Rails integration
650
650
  def digestor_available?
651
- defined?(Rails) && Rails.version =~ /^[45678]/
651
+ defined?(::Rabl::Digestor)
652
652
  end
653
653
 
654
654
  def valid_format?(format)
data/lib/rabl/railtie.rb CHANGED
@@ -5,18 +5,16 @@ module Rabl
5
5
  initializer "rabl.initialize" do |app|
6
6
  # Force Rails to load view templates even in API mode
7
7
  # Stolen shamelessly from jbuilder: https://github.com/rails/jbuilder/blob/master/lib/jbuilder/railtie.rb
8
- if Rails::VERSION::MAJOR >= 5
9
- module ::ActionController
10
- module ApiRendering
11
- include ActionView::Rendering
12
- end
8
+ module ::ActionController
9
+ module ApiRendering
10
+ include ActionView::Rendering
13
11
  end
12
+ end
14
13
 
15
- ActiveSupport.on_load :action_controller do
16
- if self == ActionController::API
17
- include ActionController::Helpers
18
- include ActionController::ImplicitRender
19
- end
14
+ ActiveSupport.on_load :action_controller do
15
+ if self == ActionController::API
16
+ include ActionController::Helpers
17
+ include ActionController::ImplicitRender
20
18
  end
21
19
  end
22
20
 
@@ -24,10 +22,8 @@ module Rabl
24
22
  Rabl.register!
25
23
 
26
24
  # Inject dependency tracker for :rabl
27
- if Rails.version =~ /^[45678]/
28
- require 'action_view/dependency_tracker'
29
- ActionView::DependencyTracker.register_tracker :rabl, Rabl::Tracker
30
- end
25
+ require 'action_view/dependency_tracker'
26
+ ActionView::DependencyTracker.register_tracker :rabl, Rabl::Tracker
31
27
  end
32
28
  end
33
29
  end # Railtie
data/lib/rabl/renderer.rb CHANGED
@@ -63,24 +63,26 @@ module Rabl
63
63
 
64
64
  # Returns the source given a relative template path
65
65
  def process_source(source)
66
- return source if source.is_a?(String) && source =~ /\n/
66
+ return source if source.is_a?(String) && source.include?("\n")
67
67
 
68
68
  source, location = engine.fetch_source(source, { :view_path => options[:view_path] })
69
69
  options[:source_location] ||= location
70
70
  source
71
71
  end
72
72
 
73
- # Internal: Sets an instance variable named after the class of `object`
73
+ # Internal: Sets an instance variable named after the class of `object`,
74
+ # unless the class has no name (Struct.new or Class.new)
74
75
  #
75
76
  # Example:
76
77
  # object.class.name # => User
77
78
  # set_object_instance_variable # => @user == object
78
79
  #
79
80
  def set_object_instance_variable
80
- instance_variable_set(:"@#{object_model_name}", object)
81
+ name = object_model_name
82
+ instance_variable_set(:"@#{name}", object) if name
81
83
  end
82
84
 
83
- # Internal: Returns the model name for an object
85
+ # Internal: Returns the model name for an object, or nil when its class has no name
84
86
  #
85
87
  # Example:
86
88
  # object.class.name # => User
@@ -92,7 +94,10 @@ module Rabl
92
94
  is_collection = item.is_a?(Array)
93
95
  item = item.first if is_collection
94
96
 
95
- name = item.class.name.underscore
97
+ name = item.class.name
98
+ return unless name
99
+
100
+ name = name.underscore
96
101
 
97
102
  name = name.pluralize if is_collection
98
103
 
data/lib/rabl/sources.rb CHANGED
@@ -28,7 +28,10 @@ module Rabl
28
28
  # fetch_source("show", :view_path => "...") => "...contents..."
29
29
  def fetch_source(file, options = {})
30
30
  resolver = source_resolver
31
+ # the process-wide cache keeps a number for each lookup for the life of the process; a
32
+ # collection that lives for one render only needs the lookup itself
31
33
  lookup = source_lookup(resolver) if Rabl.configuration.cache_sources
34
+ lookup = render_source_lookup(resolver) if lookup.nil? && Rabl.render_source_cache
32
35
  # a lookup that can't be told apart from others (see source_lookup) isn't cached
33
36
  return resolve_source(file, options, resolver) if lookup == false
34
37
 
@@ -85,9 +88,11 @@ module Rabl
85
88
  # details such as versioncake's versions) and the view paths; elsewhere the format
86
89
  # and locales the manual lookup prefers, and under Sinatra the app's views. Returns
87
90
  # false when a view path has no stable name, so the lookup isn't cached.
88
- # ceiling: outside Rails the cache (and MANUAL_LOOKUP_IDS) keeps an entry per locale looked
89
- # up in, as Rails' details keys do, so it is bounded by I18n.available_locales only while
90
- # I18n.enforce_available_locales is on (the default).
91
+ # ceiling: with cache_sources on, the cache, LOOKUP_IDS and MANUAL_LOOKUP_IDS keep an entry
92
+ # per distinct set of lookup details and view path list, and per locale looked up in, as
93
+ # Rails' own registry does, so outside Rails they are bounded by I18n.available_locales only
94
+ # while I18n.enforce_available_locales is on (the default) and by the locales requests ask
95
+ # for; under Rails a view path prepended for one request adds one, as it does to Rails.
91
96
  def source_lookup(resolver)
92
97
  case resolver
93
98
  when :rails
@@ -102,6 +107,38 @@ module Rabl
102
107
  end
103
108
  end
104
109
 
110
+ # Returns the lookup of the per-render collection: the same as source_lookup, without the
111
+ # numbers LOOKUP_IDS, MANUAL_LOOKUP_IDS and I18n.fallbacks keep for the life of the process.
112
+ # Outside Rails those gain an entry per locale looked up in, which is bounded only while
113
+ # I18n.enforce_available_locales is on, and a render that ends drops its collection anyway.
114
+ # Under Rails the numbers come from the lookup details Rails itself keeps, so they are used as
115
+ # they are. Padrino's lookup depends on the request too (its controller and content type), so
116
+ # it is not collected per render, as with cache_sources off.
117
+ def render_source_lookup(resolver)
118
+ case resolver
119
+ when :rails
120
+ source_lookup(resolver)
121
+ when :sinatra
122
+ [context_scope.settings.views, manual_lookup]
123
+ when :padrino
124
+ false
125
+ else
126
+ manual_lookup
127
+ end
128
+ end
129
+
130
+ # Returns what the manual lookup depends on, as values rather than as the number standing for
131
+ # it, and where the locale's fallbacks come from rather than the chain itself: asking I18n for
132
+ # a locale's fallbacks keeps that locale
133
+ def manual_lookup
134
+ format = manual_source_format
135
+ return [:manual, format] unless defined?(::I18n) && ::I18n.respond_to?(:config)
136
+
137
+ config = ::I18n.config
138
+ fallbacks = ::I18n.fallbacks if ::I18n.respond_to?(:fallbacks)
139
+ [:manual, format, config.locale, config.default_locale, fallbacks && fallbacks.__id__]
140
+ end
141
+
105
142
  # Returns the number standing for the format and locales the manual lookup prefers.
106
143
  # Nested Hashes of what the locales come from, as this runs on every source cache hit.
107
144
  def manual_lookup_id
@@ -189,7 +226,7 @@ module Rabl
189
226
  end
190
227
  end
191
228
 
192
- # Returns the format Rails 3.2 and higher look templates up in:
229
+ # Returns the format Rails looks templates up in:
193
230
  # the request's format, or for HTML requests the format being rendered (json for
194
231
  # the default handler and for hash), as a Symbol, the only form Rails 7.0 and higher match
195
232
  def rails_source_format
@@ -252,26 +289,18 @@ module Rabl
252
289
  File.join(view_path.first.to_s, (file_path.to_s + ".rabl"))
253
290
  end
254
291
 
255
- # Returns the rabl template path for Rails, including special lookups for Rails 2 and 3
292
+ # Returns the rabl template path for Rails, using Rails' own lookup
256
293
  def fetch_rails_source(file, options = {})
257
- # use Rails template resolution mechanism if possible (find_template)
258
294
  source_format = request_format if defined?(request_format)
259
295
 
260
- if source_format && context_scope.respond_to?(:lookup_context) # Rails 3
296
+ if source_format && context_scope.respond_to?(:lookup_context)
261
297
  lookup_proc = lambda do |partial|
262
- if ActionPack::VERSION::MAJOR == 3 && ActionPack::VERSION::MINOR < 2
263
- context_scope.lookup_context.find(file, [], partial)
264
- else # Rails 3.2 and higher
265
- # pull format directly from rails unless it is html
266
- context_scope.lookup_context.find(file, [], partial, [], { :formats => [rails_source_format] })
267
- end
298
+ # pull format directly from rails unless it is html
299
+ context_scope.lookup_context.find(file, [], partial, [], { :formats => [rails_source_format] })
268
300
  end
269
301
  template = lookup_proc.call(false) rescue nil
270
302
  template ||= lookup_proc.call(true) rescue nil
271
303
  template.identifier if template
272
- elsif source_format && context_scope.respond_to?(:view_paths) # Rails 2
273
- template = context_scope.view_paths.find_template(file, source_format, false)
274
- template.filename if template
275
304
  end
276
305
  end
277
306
 
data/lib/rabl/template.rb CHANGED
@@ -22,51 +22,8 @@ if defined?(Tilt)
22
22
  Tilt.register 'rabl', RablTemplate
23
23
  end
24
24
 
25
- # Rails 2.X Template
26
- if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version) && Rails.version.to_s =~ /^2/
27
- require 'action_view/base'
28
- require 'action_view/template'
29
-
30
- module ActionView
31
- module TemplateHandlers
32
- class RablHandler < TemplateHandler
33
- include Compilable
34
-
35
- def compile(template) %{
36
- ::Rabl::Engine.new(#{template.source.inspect}, { :format => #{template.format.inspect} }).
37
- render(self, assigns.merge(local_assigns))
38
- } end
39
- end
40
- end
41
- end
42
-
43
- ActionView::Template.register_template_handler :rabl, ActionView::TemplateHandlers::RablHandler
44
- end
45
-
46
- # Rails 3.X / 4.X / 5.X Template
47
- if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version) && Rails.version.to_s =~ /^[345]/
48
- module ActionView
49
- module Template::Handlers
50
- class Rabl
51
- class_attribute :default_format
52
- self.default_format = Mime[:json]
53
-
54
- def self.call(template)
55
- source = template.source
56
-
57
- %{ ::Rabl::Engine.new(#{source.inspect}, :source_location => #{template.identifier.inspect}).
58
- apply(self, assigns.merge(local_assigns)).
59
- render }
60
- end # call
61
- end # rabl class
62
- end # handlers
63
- end
64
-
65
- ActionView::Template.register_template_handler :rabl, ActionView::Template::Handlers::Rabl
66
- end
67
-
68
- # Rails 6.X / 7.X / 8.X Template
69
- if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version) && Rails.version.to_s =~ /^[678]/
25
+ # Rails Template
26
+ if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version)
70
27
  module ActionView
71
28
  module Template::Handlers
72
29
  class Rabl
data/lib/rabl/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rabl
4
- VERSION = "0.18.0"
4
+ VERSION = "0.19.1"
5
5
  end
data/lib/rabl.rb CHANGED
@@ -21,13 +21,17 @@ require 'rabl/cache_engine'
21
21
  require 'rabl/template_cache_key'
22
22
 
23
23
  if defined?(Rails) && Rails.respond_to?(:version)
24
- require 'rabl/tracker' if Rails.version =~ /^[45678]/
25
- require 'rabl/digestor' if Rails.version =~ /^[45678]/
26
- require 'rabl/railtie' if Rails.version =~ /^[345678]/
24
+ require 'rabl/tracker'
25
+ require 'rabl/digestor'
26
+ require 'rabl/railtie'
27
27
  end
28
28
 
29
29
  # Rabl.register!
30
30
  module Rabl
31
+ # Fiber-local key of the sources read by the render in progress (see Rabl.begin_render_source_cache)
32
+ RENDER_SOURCE_CACHE = :_rabl_render_source_cache
33
+ private_constant :RENDER_SOURCE_CACHE
34
+
31
35
  class << self
32
36
 
33
37
  # Initialize RABL within an application
@@ -62,14 +66,15 @@ module Rabl
62
66
  # everything the lookup depends on (the file, the view path and the lookup details)
63
67
  # source_cache("users/index", ["path/to/view"], [:json]) { "/full/path/to/template/users/index" }
64
68
  def source_cache(file, view_path, lookup = nil)
65
- return yield unless Rabl.configuration.cache_sources
66
-
67
- @_source_cache ||= {}
69
+ # With cache_sources off, a template the collection uses for each item (extends, partial) is
70
+ # still read and resolved once per render rather than once per item. That cache belongs to
71
+ # the render, so a template changed between renders is still read again.
72
+ cache = Rabl.configuration.cache_sources ? (@_source_cache ||= {}) : render_source_cache
73
+ return yield unless cache
68
74
 
69
75
  # stores a frozen copy of the key, so later changes to the caller's strings don't reach it
70
- @_source_cache.fetch([file, view_path, lookup]) do
71
- @_source_cache[frozen_copy([file, view_path, lookup])] = yield
72
- end
76
+ key = [file, view_path, lookup]
77
+ cache.fetch(key) { cache[frozen_copy(key)] = yield }
73
78
  end
74
79
 
75
80
  # Resets the RABL source cache
@@ -77,6 +82,23 @@ module Rabl
77
82
  @_source_cache = {}
78
83
  end
79
84
 
85
+ # The template sources read so far by the render running in this fiber, if any
86
+ def render_source_cache
87
+ Thread.current[RENDER_SOURCE_CACHE]
88
+ end
89
+
90
+ # Internal: Starts collecting the sources the render about to run reads (see source_cache).
91
+ # The render that starts a collection ends it, so a render started inside another collects
92
+ # its own sources for as long as it runs and the render around it reads again afterwards.
93
+ def begin_render_source_cache
94
+ Thread.current[RENDER_SOURCE_CACHE] = {}
95
+ end
96
+
97
+ # Internal: Drops the collection the render that returns from it started.
98
+ def end_render_source_cache
99
+ Thread.current[RENDER_SOURCE_CACHE] = nil
100
+ end
101
+
80
102
  # Returns a copy of value whose Strings and Arrays are frozen, for cache keys that
81
103
  # must not change when the caller's objects do
82
104
  def frozen_copy(value)
@@ -99,6 +121,4 @@ end
99
121
  if defined?(Padrino)
100
122
  require 'padrino-core'
101
123
  Padrino.after_load { Rabl.register! }
102
- elsif defined?(Rails.version) && Rails.version =~ /^2/
103
- Rabl.register!
104
124
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rabl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.19.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Esquenazi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-31 00:00:00.000000000 Z
11
+ date: 2026-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 2.3.14
19
+ version: '6.1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 2.3.14
26
+ version: '6.1'
27
27
  description: General ruby templating with json, bson, xml and msgpack support
28
28
  email:
29
29
  - nesquena@gmail.com
@@ -35,14 +35,12 @@ files:
35
35
  - CONTRIBUTING.md
36
36
  - MIT-LICENSE
37
37
  - README.md
38
+ - SECURITY.md
38
39
  - lib/rabl.rb
39
40
  - lib/rabl/builder.rb
40
41
  - lib/rabl/cache_engine.rb
41
42
  - lib/rabl/configuration.rb
42
43
  - lib/rabl/digestor.rb
43
- - lib/rabl/digestor/rails3.rb
44
- - lib/rabl/digestor/rails41.rb
45
- - lib/rabl/digestor/rails5.rb
46
44
  - lib/rabl/engine.rb
47
45
  - lib/rabl/helpers.rb
48
46
  - lib/rabl/multi_builder.rb
@@ -57,7 +55,11 @@ files:
57
55
  homepage: https://github.com/nesquena/rabl
58
56
  licenses:
59
57
  - MIT
60
- metadata: {}
58
+ metadata:
59
+ changelog_uri: https://github.com/nesquena/rabl/blob/master/CHANGELOG.md
60
+ source_code_uri: https://github.com/nesquena/rabl
61
+ bug_tracker_uri: https://github.com/nesquena/rabl/issues
62
+ rubygems_mfa_required: 'true'
61
63
  post_install_message:
62
64
  rdoc_options: []
63
65
  require_paths:
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'action_view'
4
-
5
- module Rabl
6
- class Digestor < ActionView::Digestor
7
- def self.digest(name, format, finder, options = {})
8
- cache_key = [name, format] + Array.wrap(options[:dependencies])
9
- @@cache[cache_key.join('.')] ||= begin
10
- Digestor.new(name, format, finder, options).digest
11
- end
12
- end
13
- end
14
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'action_view'
4
-
5
- module Rabl
6
- class Digestor < ActionView::Digestor
7
- def self.digest(options = {})
8
- cache_key = [options[:name]] + Array.wrap(options[:dependencies])
9
- @@cache[cache_key.join('.')] ||= begin
10
- Digestor.new({ :name => options[:name], :finder => options[:finder] }.merge!(options)).digest
11
- end
12
- end
13
- end
14
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'action_view'
4
-
5
- module Rabl
6
- class Digestor < ActionView::Digestor
7
- end
8
- end