From 690700d6b32f3ca4986b9b6931583c7411251aea Mon Sep 17 00:00:00 2001 From: "jingyuan.zhao" Date: Mon, 26 Jan 2026 19:28:18 +0900 Subject: [PATCH 1/6] Define template lambdas into methods in Renderer. --- lib/simple_json/simple_json_renderer.rb | 30 ++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/simple_json/simple_json_renderer.rb b/lib/simple_json/simple_json_renderer.rb index ba4e1a3..b8d76f4 100644 --- a/lib/simple_json/simple_json_renderer.rb +++ b/lib/simple_json/simple_json_renderer.rb @@ -20,7 +20,7 @@ def load_all_templates! template_files = Rails.root.glob("#{path}/**/*.simple_json.rb") template_files.each do |file_path| template_path = file_path.relative_path_from(Rails.root.join(path)).to_path.delete_suffix('.simple_json.rb') - @renderers[template_path] = SimpleJsonTemplate.new(file_path.to_path).renderer + define_template_method(template_path, file_path.to_path) end end @templates_loaded = true @@ -29,7 +29,7 @@ def load_all_templates! def load_template(template_path) if SimpleJson.template_cache_enabled? load_all_templates! unless templates_loaded? - renderers[template_path] + render_methods[template_path] else load_template_from_file(template_path) end @@ -38,18 +38,31 @@ def load_template(template_path) def load_template_from_file(template_path) SimpleJson.template_paths.each do |path| file_path = Rails.root.join("#{path}/#{template_path}.simple_json.rb").to_path - return SimpleJsonTemplate.new(file_path).renderer if File.exist?(file_path) + + if File.exist?(file_path) + return define_template_method(template_path, file_path) + end end nil end - def renderers - @renderers ||= {} + def define_template_method(template_path, file_path) + @template_num ||= 0 + @template_num += 1 + method_name = :"template_#{@template_num}" + render_methods[template_path] = method_name + define_method(method_name, &SimpleJsonTemplate.new(file_path).renderer) + + return method_name + end + + def render_methods + @render_methods ||= {} end def clear_renderers - @renderers = {} + @render_methods = {} @templates_loaded = false end end @@ -70,10 +83,11 @@ def renderers end def render(template_name, **params) + method_name = renderer(template_name) if !params.empty? - instance_exec(**params, &renderer(template_name)) + send(method_name, **params) else - instance_exec(&renderer(template_name)) + send(method_name) end end From cea163c966aaabf785d7053beccf3fe1eb223a3c Mon Sep 17 00:00:00 2001 From: "jingyuan.zhao" Date: Mon, 26 Jan 2026 19:34:59 +0900 Subject: [PATCH 2/6] Rename Template.renderer to lambda --- lib/simple_json/simple_json_renderer.rb | 2 +- lib/simple_json/simple_json_template.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/simple_json/simple_json_renderer.rb b/lib/simple_json/simple_json_renderer.rb index b8d76f4..1b098d1 100644 --- a/lib/simple_json/simple_json_renderer.rb +++ b/lib/simple_json/simple_json_renderer.rb @@ -52,7 +52,7 @@ def define_template_method(template_path, file_path) @template_num += 1 method_name = :"template_#{@template_num}" render_methods[template_path] = method_name - define_method(method_name, &SimpleJsonTemplate.new(file_path).renderer) + define_method(method_name, &SimpleJsonTemplate.new(file_path).lambda) return method_name end diff --git a/lib/simple_json/simple_json_template.rb b/lib/simple_json/simple_json_template.rb index deab7b3..e64a9e5 100644 --- a/lib/simple_json/simple_json_template.rb +++ b/lib/simple_json/simple_json_template.rb @@ -7,8 +7,8 @@ def initialize(path) @source = File.read(path) end - def renderer - @renderer ||= eval(code, TOPLEVEL_BINDING, @path) # rubocop:disable Security/Eval + def lambda + @lambda ||= eval(code, TOPLEVEL_BINDING, @path) # rubocop:disable Security/Eval end def code From 334260ef62f0b52db717f724e723b2df4b5317b1 Mon Sep 17 00:00:00 2001 From: "jingyuan.zhao" Date: Tue, 27 Jan 2026 11:47:00 +0900 Subject: [PATCH 3/6] Define method using class_eval instead of define_method. --- lib/simple_json/simple_json_renderer.rb | 8 ++---- lib/simple_json/simple_json_template.rb | 33 ++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/simple_json/simple_json_renderer.rb b/lib/simple_json/simple_json_renderer.rb index 1b098d1..9f1c35c 100644 --- a/lib/simple_json/simple_json_renderer.rb +++ b/lib/simple_json/simple_json_renderer.rb @@ -39,9 +39,7 @@ def load_template_from_file(template_path) SimpleJson.template_paths.each do |path| file_path = Rails.root.join("#{path}/#{template_path}.simple_json.rb").to_path - if File.exist?(file_path) - return define_template_method(template_path, file_path) - end + return define_template_method(template_path, file_path) if File.exist?(file_path) end nil @@ -52,9 +50,7 @@ def define_template_method(template_path, file_path) @template_num += 1 method_name = :"template_#{@template_num}" render_methods[template_path] = method_name - define_method(method_name, &SimpleJsonTemplate.new(file_path).lambda) - - return method_name + SimpleJsonTemplate.new(file_path).define_to_class(self, method_name) end def render_methods diff --git a/lib/simple_json/simple_json_template.rb b/lib/simple_json/simple_json_template.rb index e64a9e5..37d3e2d 100644 --- a/lib/simple_json/simple_json_template.rb +++ b/lib/simple_json/simple_json_template.rb @@ -7,20 +7,45 @@ def initialize(path) @source = File.read(path) end - def lambda - @lambda ||= eval(code, TOPLEVEL_BINDING, @path) # rubocop:disable Security/Eval + def define_to_class(klass, method_name) + method_string = to_method_string(method_name) + klass.class_eval(method_string, @path) + + method_name end + private + def code @code ||= lambda_stringify(@source) end - private - def lambda_stringify(source) return source if source.match?(/^(?:\s*(?:#.*?)?\n)*\s*->/) "-> { #{source} }" end + + def method_string_from_lambda(source, method_name) + pattern = %r{ + \A(?(?:\s*\#.*\n|\s+)*) + ->\s* + (?:\((?.*?)\)|(?[^\{ ]*?)) + \s*(?:\{(?.*)\}|do(?.*)end) + (?(?:\s*\#.*|\s+)*)\z + }mx + + match = source.match(pattern) + raise :parse_error unless match + + params = (match[:params_p] || match[:params_n] || "").strip + arg_part = params.empty? ? "" : "(#{params})" + + "#{match[:prefix]}def #{method_name}#{arg_part};#{match[:body]};end#{match[:suffix]}" + end + + def to_method_string(method_name) + method_string_from_lambda(code, method_name) + end end end From 1c3bb9fdda8c342a4bf97e16fcf168f7b31e2b21 Mon Sep 17 00:00:00 2001 From: "jingyuan.zhao" Date: Tue, 27 Jan 2026 21:00:59 +0900 Subject: [PATCH 4/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c3c0a53..ec5a6dc 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ This project is built on work of [jb](https://github.com/amatsuda/jb). ## Template Syntax -SimpleJson templates are simply lambda objects that return data(Hashes or Arrays) for json. +SimpleJson templates should be written in Ruby lambda format. The template code is converted into a method and then invoked to produce data (Hashes or Arrays) for JSON. ```ruby -> { From 917e03d71ceaa498d850d0d4d7362d126547f067 Mon Sep 17 00:00:00 2001 From: "jingyuan.zhao" Date: Wed, 28 Jan 2026 12:44:57 +0900 Subject: [PATCH 5/6] Reuse methods when template cache disabled --- lib/simple_json/simple_json_renderer.rb | 19 ++++++++++++++----- lib/simple_json/simple_json_template.rb | 4 ++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/simple_json/simple_json_renderer.rb b/lib/simple_json/simple_json_renderer.rb index 9f1c35c..5961765 100644 --- a/lib/simple_json/simple_json_renderer.rb +++ b/lib/simple_json/simple_json_renderer.rb @@ -46,17 +46,26 @@ def load_template_from_file(template_path) end def define_template_method(template_path, file_path) - @template_num ||= 0 - @template_num += 1 - method_name = :"template_#{@template_num}" - render_methods[template_path] = method_name - SimpleJsonTemplate.new(file_path).define_to_class(self, method_name) + template = SimpleJsonTemplate.new(file_path) + code_hash = template.code.hash + + render_methods[template_path] = render_methods_cache.fetch(code_hash) do + @template_num ||= 0 + @template_num += 1 + method_name = :"template_#{@template_num}" + template.define_to_class(self, method_name) + render_methods_cache[code_hash] = method_name + end end def render_methods @render_methods ||= {} end + def render_methods_cache + @render_methods_cache ||= {} + end + def clear_renderers @render_methods = {} @templates_loaded = false diff --git a/lib/simple_json/simple_json_template.rb b/lib/simple_json/simple_json_template.rb index 37d3e2d..a375e32 100644 --- a/lib/simple_json/simple_json_template.rb +++ b/lib/simple_json/simple_json_template.rb @@ -14,12 +14,12 @@ def define_to_class(klass, method_name) method_name end - private - def code @code ||= lambda_stringify(@source) end + private + def lambda_stringify(source) return source if source.match?(/^(?:\s*(?:#.*?)?\n)*\s*->/) From 9f6b48c63e7c2d85e7eb05af1e3c8772a2f424b7 Mon Sep 17 00:00:00 2001 From: "jingyuan.zhao" Date: Wed, 28 Jan 2026 19:39:48 +0900 Subject: [PATCH 6/6] Add test for lambda-to-method conversion. --- test/unit/simple_json_template_test.rb | 251 +++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 test/unit/simple_json_template_test.rb diff --git a/test/unit/simple_json_template_test.rb b/test/unit/simple_json_template_test.rb new file mode 100644 index 0000000..6d6b837 --- /dev/null +++ b/test/unit/simple_json_template_test.rb @@ -0,0 +1,251 @@ +# frozen_string_literal: true + +require 'test_helper' + +class SimpleJsonTemplateTest < Test::Unit::TestCase + # -> { body } form + test 'converts simple lambda with braces' do + source = '-> { "hello" }' + result = call_method(source, :foo) + + assert_equal 'def foo; "hello" ;end', result + end + + # ->(params) { body } form (parenthesized parameters) + test 'converts lambda with parenthesized parameters' do + source = '->(a, b) { a + b }' + result = call_method(source, :add) + + assert_equal 'def add(a, b); a + b ;end', result + end + + # ->params { body } form (no parentheses, no space) + test 'converts lambda with parameter without parentheses' do + source = '->a { a * 2 }' + result = call_method(source, :double) + + assert_equal 'def double(a); a * 2 ;end', result + end + + # -> do body end form + test 'converts lambda with do-end' do + source = '-> do "hello" end' + result = call_method(source, :foo) + + assert_equal 'def foo; "hello" ;end', result + end + + # ->(params) do body end form + test 'converts lambda with parameters and do-end' do + source = '->(x) do x * 2 end' + result = call_method(source, :double) + + assert_equal 'def double(x); x * 2 ;end', result + end + + # Multiline body + test 'converts multiline lambda body' do + source = <<~RUBY + -> { + x = 1 + y = 2 + x + y + } + RUBY + result = call_method(source, :calc) + + assert_match(/def calc;/, result) + assert_match(/x = 1/, result) + assert_match(/y = 2/, result) + assert_match(/x \+ y/, result) + assert_match(/;end/, result) + end + + # Preserve prefix comments + test 'preserves prefix comments' do + source = <<~RUBY + # This is a comment + -> { "hello" } + RUBY + result = call_method(source, :foo) + + assert_match(/\A# This is a comment\n/, result) + assert_match(/def foo;/, result) + end + + # Preserve suffix comments + test 'preserves suffix comments' do + source = <<~RUBY + -> { "hello" } + # trailing comment + RUBY + result = call_method(source, :foo) + + assert_match(/;end\n# trailing comment/, result) + end + + # Empty parameter list + test 'converts lambda with empty parentheses' do + source = '->() { "hello" }' + result = call_method(source, :foo) + + assert_equal 'def foo; "hello" ;end', result + end + + # Space before brace with no parameters + test 'converts lambda with space before brace' do + source = '-> { "hello" }' + result = call_method(source, :foo) + + assert_equal 'def foo; "hello" ;end', result + end + + # Multiline do-end form + test 'converts multiline do-end lambda' do + source = <<~RUBY + -> do + result = [] + result << 1 + result + end + RUBY + result = call_method(source, :build) + + assert_match(/def build;/, result) + assert_match(/result = \[\]/, result) + assert_match(/;end/, result) + end + + # --- Parameter types --- + + # Default argument + test 'converts lambda with default parameter' do + source = '->(a, b = 1) { a + b }' + result = call_method(source, :add) + + assert_equal 'def add(a, b = 1); a + b ;end', result + end + + # Keyword arguments + test 'converts lambda with keyword parameters' do + source = '->(a:, b: 1) { a + b }' + result = call_method(source, :add) + + assert_equal 'def add(a:, b: 1); a + b ;end', result + end + + # Variadic arguments + test 'converts lambda with splat parameter' do + source = '->(*args) { args }' + result = call_method(source, :collect) + + assert_equal 'def collect(*args); args ;end', result + end + + # Block parameter + test 'converts lambda with block parameter' do + source = '->(&block) { block.call }' + result = call_method(source, :execute) + + assert_equal 'def execute(&block); block.call ;end', result + end + + # Double splat + test 'converts lambda with double splat parameter' do + source = '->(**opts) { opts }' + result = call_method(source, :options) + + assert_equal 'def options(**opts); opts ;end', result + end + + # --- Body edge cases --- + + # Nested braces + test 'converts lambda with nested braces in body' do + source = '-> { { key: value } }' + result = call_method(source, :hash) + + assert_equal 'def hash; { key: value } ;end', result + end + + # Empty body (braces) + test 'converts lambda with empty body' do + source = '-> { }' + result = call_method(source, :noop) + + assert_equal 'def noop; ;end', result + end + + # Empty body (do-end) + test 'converts do-end lambda with empty body' do + source = '-> do end' + result = call_method(source, :noop) + + assert_equal 'def noop; ;end', result + end + + # do-end body contains end keyword + test 'converts do-end lambda with nested end keyword' do + source = '-> do if x then y end end' + result = call_method(source, :conditional) + + assert_equal 'def conditional; if x then y end ;end', result + end + + # Body string contains end + test 'converts lambda with end string in body' do + source = '-> { "end of string" }' + result = call_method(source, :message) + + assert_equal 'def message; "end of string" ;end', result + end + + # --- Prefix edge cases --- + + # Multiple prefix comments + test 'preserves multiple prefix comments' do + source = <<~RUBY + # comment 1 + # comment 2 + -> { 1 } + RUBY + result = call_method(source, :foo) + + assert_match(/\A# comment 1\n# comment 2\n/, result) + assert_match(/def foo;/, result) + end + + # Blank line prefix + test 'preserves blank line prefix' do + source = "\n\n-> { 1 }" + result = call_method(source, :foo) + + assert_match(/\A\n\ndef foo;/, result) + end + + # --- Error cases --- + + # Non-lambda input + test 'raises error for non-lambda input' do + source = 'def foo; end' + + assert_raise(TypeError) do + call_method(source, :foo) + end + end + + # Proc.new input + test 'raises error for Proc.new input' do + source = 'Proc.new { 1 }' + + assert_raise(TypeError) do + call_method(source, :foo) + end + end + + private + + def call_method(source, method_name) + SimpleJson::SimpleJsonTemplate.allocate.send(:method_string_from_lambda, source, method_name) + end +end