From 7c58bcc5dcddec8695e938a35e4c4c581c08f537 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Fri, 30 May 2025 10:11:05 -0400 Subject: [PATCH 1/9] Optimize key formatter --- lib/jbuilder/key_formatter.rb | 36 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/lib/jbuilder/key_formatter.rb b/lib/jbuilder/key_formatter.rb index c9e7eca3..5d4a6e76 100644 --- a/lib/jbuilder/key_formatter.rb +++ b/lib/jbuilder/key_formatter.rb @@ -1,34 +1,26 @@ require 'jbuilder/jbuilder' -require 'active_support/core_ext/array' class Jbuilder class KeyFormatter - def initialize(*args) - @format = {} - @cache = {} + def initialize(*formats, **formats_with_options) + @cache = + Hash.new do |hash, key| + value = key.is_a?(Symbol) ? key.name : key.to_s - options = args.extract_options! - args.each do |name| - @format[name] = [] - end - options.each do |name, parameters| - @format[name] = parameters - end - end + formats.each do |func| + value = func.is_a?(Proc) ? func.call(value) : value.send(func) + end + + formats_with_options.each do |func, params| + value = func.is_a?(Proc) ? func.call(value, *params) : value.send(func, *params) + end - def initialize_copy(original) - @cache = {} + hash[key] = value + end end def format(key) - @cache[key] ||= @format.inject(key.to_s) do |result, args| - func, args = args - if ::Proc === func - func.call result, *args - else - result.send func, *args - end - end + @cache[key] end end end From b524e6c5a28af10302cc278cf7a1ef7e06038db1 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Fri, 30 May 2025 10:11:45 -0400 Subject: [PATCH 2/9] Reuse key cache across template renders --- lib/jbuilder.rb | 6 +++--- test/jbuilder_test.rb | 8 -------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index b12624be..4d183376 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -15,9 +15,9 @@ class Jbuilder def initialize(options = {}) @attributes = {} - @key_formatter = options.fetch(:key_formatter){ @@key_formatter ? @@key_formatter.clone : nil} - @ignore_nil = options.fetch(:ignore_nil, @@ignore_nil) - @deep_format_keys = options.fetch(:deep_format_keys, @@deep_format_keys) + @key_formatter = options&.[](:key_formatter) || @@key_formatter + @ignore_nil = options&.[](:ignore_nil) || @@ignore_nil + @deep_format_keys = options&.[](:deep_format_keys) || @@deep_format_keys yield self if ::Kernel.block_given? end diff --git a/test/jbuilder_test.rb b/test/jbuilder_test.rb index 76569bb6..34156c30 100644 --- a/test/jbuilder_test.rb +++ b/test/jbuilder_test.rb @@ -784,14 +784,6 @@ class JbuilderTest < ActiveSupport::TestCase assert_equal ['camelStyle'], result.keys end - test 'do not use default key formatter directly' do - Jbuilder.key_format - jbuild{ |json| json.key 'value' } - formatter = Jbuilder.send(:class_variable_get, '@@key_formatter') - cache = formatter.instance_variable_get('@cache') - assert_empty cache - end - test 'ignore_nil! without a parameter' do result = jbuild do |json| json.ignore_nil! From 47824afc270b3879635eade5243e497860aff866 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Fri, 30 May 2025 10:12:02 -0400 Subject: [PATCH 3/9] Save on some memory allocation during initialization --- lib/jbuilder.rb | 10 +++++----- lib/jbuilder/jbuilder_template.rb | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index 4d183376..5b226862 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -12,7 +12,7 @@ class Jbuilder @@ignore_nil = false @@deep_format_keys = false - def initialize(options = {}) + def initialize(options = nil) @attributes = {} @key_formatter = options&.[](:key_formatter) || @@key_formatter @@ -100,13 +100,13 @@ def method_missing(*args, &block) # # { "_first_name": "David" } # - def key_format!(*args) - @key_formatter = KeyFormatter.new(*args) + def key_format!(...) + @key_formatter = KeyFormatter.new(...) end # Same as the instance method key_format! except sets the default. - def self.key_format(*args) - @@key_formatter = KeyFormatter.new(*args) + def self.key_format(...) + @@key_formatter = KeyFormatter.new(...) end # If you want to skip adding nil values to your JSON hash. This is useful diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index 55f2d5ff..ec4d7b57 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -10,10 +10,10 @@ class << self self.template_lookup_options = { handlers: [:jbuilder] } - def initialize(context, *args) + def initialize(context, options = nil) @context = context @cached_root = nil - super(*args) + super(options) end # Generates JSON using the template specified with the `:partial` option. For example, the code below will render From a6f1eedc435040e94b11230ab405a3b8ee1f5f1f Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Mon, 2 Jun 2025 16:05:30 -0400 Subject: [PATCH 4/9] Use mutex for thread safety --- lib/jbuilder/key_formatter.rb | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/jbuilder/key_formatter.rb b/lib/jbuilder/key_formatter.rb index 5d4a6e76..2c0f1f99 100644 --- a/lib/jbuilder/key_formatter.rb +++ b/lib/jbuilder/key_formatter.rb @@ -3,24 +3,28 @@ class Jbuilder class KeyFormatter def initialize(*formats, **formats_with_options) - @cache = - Hash.new do |hash, key| + @mutex = Mutex.new + @formats = formats + @formats_with_options = formats_with_options + @cache = {} + end + + def format(key) + @mutex.synchronize do + @cache[key] ||= begin value = key.is_a?(Symbol) ? key.name : key.to_s - formats.each do |func| + @formats.each do |func| value = func.is_a?(Proc) ? func.call(value) : value.send(func) end - formats_with_options.each do |func, params| + @formats_with_options.each do |func, params| value = func.is_a?(Proc) ? func.call(value, *params) : value.send(func, *params) end - hash[key] = value + value end - end - - def format(key) - @cache[key] + end end end end From 964ef0b794106bd00525dd7a408817f0dbf29902 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Mon, 2 Jun 2025 16:47:52 -0400 Subject: [PATCH 5/9] Optimize jbuilder initialization --- lib/jbuilder.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index 5b226862..382ad3d5 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -15,9 +15,15 @@ class Jbuilder def initialize(options = nil) @attributes = {} - @key_formatter = options&.[](:key_formatter) || @@key_formatter - @ignore_nil = options&.[](:ignore_nil) || @@ignore_nil - @deep_format_keys = options&.[](:deep_format_keys) || @@deep_format_keys + if options + @key_formatter = options[:key_formatter] + @ignore_nil = options[:ignore_nil] + @deep_format_keys = options[:deep_format_keys] + else + @key_formatter = @@key_formatter + @ignore_nil = @@ignore_nil + @deep_format_keys = @@deep_format_keys + end yield self if ::Kernel.block_given? end From 34814e78bda685b0ef9f4a69b6208dd71e263cf7 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Tue, 3 Jun 2025 10:34:23 -0400 Subject: [PATCH 6/9] Repurpose test to validate default formatter cache is used --- test/jbuilder_test.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/jbuilder_test.rb b/test/jbuilder_test.rb index 34156c30..4f2cbe20 100644 --- a/test/jbuilder_test.rb +++ b/test/jbuilder_test.rb @@ -784,6 +784,14 @@ class JbuilderTest < ActiveSupport::TestCase assert_equal ['camelStyle'], result.keys end + test 'use default key formatter when configured' do + Jbuilder.key_format + jbuild{ |json| json.key 'value' } + formatter = Jbuilder.send(:class_variable_get, '@@key_formatter') + cache = formatter.instance_variable_get('@cache') + assert_includes cache, :key + end + test 'ignore_nil! without a parameter' do result = jbuild do |json| json.ignore_nil! From 5ea899be843106822662bdc3ffbf8771ac7ae5be Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Tue, 3 Jun 2025 10:37:44 -0400 Subject: [PATCH 7/9] Invert options nil check --- lib/jbuilder.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index 382ad3d5..cc3f5f05 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -15,14 +15,14 @@ class Jbuilder def initialize(options = nil) @attributes = {} - if options - @key_formatter = options[:key_formatter] - @ignore_nil = options[:ignore_nil] - @deep_format_keys = options[:deep_format_keys] - else + if options.nil? @key_formatter = @@key_formatter @ignore_nil = @@ignore_nil @deep_format_keys = @@deep_format_keys + else + @key_formatter = options[:key_formatter] + @ignore_nil = options[:ignore_nil] + @deep_format_keys = options[:deep_format_keys] end yield self if ::Kernel.block_given? From be609e7b10bac8d5c3351fd6d97afcb9560e5133 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Tue, 3 Jun 2025 10:43:18 -0400 Subject: [PATCH 8/9] Cleanup initializer --- lib/jbuilder.rb | 15 ++++----------- lib/jbuilder/jbuilder_template.rb | 3 ++- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index cc3f5f05..fad1962c 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -12,18 +12,11 @@ class Jbuilder @@ignore_nil = false @@deep_format_keys = false - def initialize(options = nil) + def initialize(key_formatter: @@key_formatter, ignore_nil: @@ignore_nil, deep_format_keys: @@deep_format_keys) @attributes = {} - - if options.nil? - @key_formatter = @@key_formatter - @ignore_nil = @@ignore_nil - @deep_format_keys = @@deep_format_keys - else - @key_formatter = options[:key_formatter] - @ignore_nil = options[:ignore_nil] - @deep_format_keys = options[:deep_format_keys] - end + @key_formatter = key_formatter + @ignore_nil = ignore_nil + @deep_format_keys = deep_format_keys yield self if ::Kernel.block_given? end diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index ec4d7b57..2984aafd 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -13,7 +13,8 @@ class << self def initialize(context, options = nil) @context = context @cached_root = nil - super(options) + + options.nil? ? super() : super(**options) end # Generates JSON using the template specified with the `:partial` option. For example, the code below will render From e0d60fdbd6dd621b523e9a1eaeea1470a84eb382 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Wed, 4 Jun 2025 10:35:28 -0400 Subject: [PATCH 9/9] Save on call to ::Kernal.block_given? --- lib/jbuilder.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index fad1962c..db2f25c8 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -12,13 +12,18 @@ class Jbuilder @@ignore_nil = false @@deep_format_keys = false - def initialize(key_formatter: @@key_formatter, ignore_nil: @@ignore_nil, deep_format_keys: @@deep_format_keys) + def initialize( + key_formatter: @@key_formatter, + ignore_nil: @@ignore_nil, + deep_format_keys: @@deep_format_keys, + &block + ) @attributes = {} @key_formatter = key_formatter @ignore_nil = ignore_nil @deep_format_keys = deep_format_keys - yield self if ::Kernel.block_given? + yield self if block end # Yields a builder and automatically turns the result into a JSON string