Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ jobs:
build:
strategy:
matrix:
ruby: ['3.2', '3.3', '3.4']
rails: ["7.1", "7.2", "8.0"]
ruby: ['3.2', '3.3', '3.4', "4.0"]
rails: ["7.1", "7.2", "8.0", "8.1"]

runs-on: ubuntu-latest

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ gemfiles/*.lock
/spec/reports/
/tmp/
/test/dummy_app/log/*
/test/dummy_app/tmp/*
/vendor/bundle
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

## [Unreleased]

## [1.0.0] - 2026-01-28
### Breaking
- Templates are no longer executed by invoking lambdas directly; they are compiled into methods.

### Changed
- Cache template methods by code hash to reduce Ruby method cache invalidation.
- Use a renderer subclass that includes helpers instead of extending instances.

### Fixed
- Rename the config key `cache_enabled` to `template_cache_enabled`.
- Add `/vendor/bundle` to `.gitignore`.

## [0.1.0] - 2025-09-10
### Changed
- Default JSON serializer switched from Oj to ActiveSupport::JSON to better align with Rails defaults.
- Development dependencies refreshed and benchmark script fixed for the latest Ruby/Rails stacks.
- README formatting and examples improved for clarity.

### Fixed
- Resolved `MissingTemplate` errors introduced by the Rails 8 upgrade.
- Added coverage for rendering when template/action names differ to avoid regressions.

## [0.0.0] - 2021-11-02
### Added
- Initial SimpleJson renderer, templates (`.simple_json.rb` lambdas), and `SimpleJson::SimpleJsonRenderable` integration.
- Template caching toggle and configurable template paths.
- Rails generator hooks and dummy app scaffolding for getting started.
- Migration helpers for comparing SimpleJson output against existing Jbuilder views.
138 changes: 54 additions & 84 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,110 +176,80 @@ Rails.application.config.generators.simple_json false

Here're the results of a benchmark (which you can find [here](https://github.com/aktsk/simple_json/blob/master/test/dummy_app/app/controllers/benchmarks_controller.rb) in this repo) rendering a collection to JSON.

### RAILS_ENV=development

```
% ./bin/benchmark.sh

* Rendering 10 partials via render_partial
Warming up --------------------------------------
jb 257.000 i/100ms
jbuilder 108.000 i/100ms
simple_json 2.039k i/100ms
Calculating -------------------------------------
jb 2.611k (± 7.1%) i/s - 13.107k in 5.046110s
jbuilder 1.084k (± 3.5%) i/s - 5.508k in 5.088845s
simple_json 20.725k (± 4.4%) i/s - 103.989k in 5.026914s

Comparison:
simple_json: 20725.5 i/s
jb: 2610.5 i/s - 7.94x (± 0.00) slower
jbuilder: 1083.8 i/s - 19.12x (± 0.00) slower


* Rendering 100 partials via render_partial
Warming up --------------------------------------
jb 88.000 i/100ms
jbuilder 14.000 i/100ms
simple_json 290.000 i/100ms
Calculating -------------------------------------
jb 928.202 (± 5.0%) i/s - 4.664k in 5.037314s
jbuilder 137.980 (± 6.5%) i/s - 700.000 in 5.094658s
simple_json 2.931k (± 5.2%) i/s - 14.790k in 5.060707s

Comparison:
simple_json: 2931.1 i/s
jb: 928.2 i/s - 3.16x (± 0.00) slower
jbuilder: 138.0 i/s - 21.24x (± 0.00) slower


* Rendering 1000 partials via render_partial
Warming up --------------------------------------
jb 11.000 i/100ms
jbuilder 1.000 i/100ms
simple_json 29.000 i/100ms
Calculating -------------------------------------
jb 106.150 (± 5.7%) i/s - 539.000 in 5.094255s
jbuilder 13.012 (± 7.7%) i/s - 65.000 in 5.054016s
simple_json 271.683 (± 5.2%) i/s - 1.363k in 5.030646s

Comparison:
simple_json: 271.7 i/s
jb: 106.1 i/s - 2.56x (± 0.00) slower
jbuilder: 13.0 i/s - 20.88x (± 0.00) slower
```

### RAILS_ENV=production

```
% RAILS_ENV=production ./bin/benchmark.sh
SimpleJson Benchmark
ruby: 4.0.1
rails: 8.1.1
json: 2.15.2
oj: 3.16.11
----------------------

* Rendering 10 partials via render_partial
* Rendering 10 partials via render_to_string
ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [arm64-darwin24]
Warming up --------------------------------------
jb 246.000 i/100ms
jbuilder 97.000 i/100ms
simple_json 1.957k i/100ms
jb 23.000 i/100ms
jbuilder 30.000 i/100ms
simple_json(oj) 40.000 i/100ms
simple_json(AS::json)
46.000 i/100ms
Calculating -------------------------------------
jb 2.611k (± 4.1%) i/s - 13.038k in 5.002304s
jbuilder 972.031 (± 4.7%) i/s - 4.850k in 5.001200s
simple_json 20.383k (± 3.8%) i/s - 101.764k in 4.999989s
jb 298.518 (±23.4%) i/s (3.35 ms/i) - 1.426k in 5.019370s
jbuilder 255.925 (± 4.3%) i/s (3.91 ms/i) - 1.290k in 5.052973s
simple_json(oj) 270.192 (± 3.7%) i/s (3.70 ms/i) - 1.360k in 5.039635s
simple_json(AS::json)
297.476 (±10.1%) i/s (3.36 ms/i) - 1.518k in 5.145803s

Comparison:
simple_json: 20382.8 i/s
jb: 2611.3 i/s - 7.81x (± 0.00) slower
jbuilder: 972.0 i/s - 20.97x (± 0.00) slower
jb: 298.5 i/s
simple_json(AS::json): 297.5 i/s - same-ish: difference falls within error
simple_json(oj): 270.2 i/s - same-ish: difference falls within error
jbuilder: 255.9 i/s - same-ish: difference falls within error


* Rendering 100 partials via render_partial
* Rendering 100 partials via render_to_string
ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [arm64-darwin24]
Warming up --------------------------------------
jb 90.000 i/100ms
jbuilder 11.000 i/100ms
simple_json 280.000 i/100ms
jb 19.000 i/100ms
jbuilder 14.000 i/100ms
simple_json(oj) 15.000 i/100ms
simple_json(AS::json)
26.000 i/100ms
Calculating -------------------------------------
jb 883.446 (± 4.8%) i/s - 4.410k in 5.003438s
jbuilder 119.932 (± 8.3%) i/s - 605.000 in 5.085382s
simple_json 2.886k (± 4.2%) i/s - 14.560k in 5.054327s
jb 186.051 (±12.9%) i/s (5.37 ms/i) - 912.000 in 5.075117s
jbuilder 144.279 (± 2.1%) i/s (6.93 ms/i) - 728.000 in 5.048538s
simple_json(oj) 159.254 (± 1.9%) i/s (6.28 ms/i) - 810.000 in 5.088178s
simple_json(AS::json)
249.690 (± 6.0%) i/s (4.00 ms/i) - 1.248k in 5.017042s

Comparison:
simple_json: 2885.7 i/s
jb: 883.4 i/s - 3.27x (± 0.00) slower
jbuilder: 119.9 i/s - 24.06x (± 0.00) slower
simple_json(AS::json): 249.7 i/s
jb: 186.1 i/s - 1.34x slower
simple_json(oj): 159.3 i/s - 1.57x slower
jbuilder: 144.3 i/s - 1.73x slower


* Rendering 1000 partials via render_partial
* Rendering 1000 partials via render_to_string
ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [arm64-darwin24]
Warming up --------------------------------------
jb 12.000 i/100ms
jbuilder 1.000 i/100ms
simple_json 32.000 i/100ms
jb 4.000 i/100ms
jbuilder 2.000 i/100ms
simple_json(oj) 2.000 i/100ms
simple_json(AS::json)
10.000 i/100ms
Calculating -------------------------------------
jb 124.627 (± 4.8%) i/s - 624.000 in 5.018515s
jbuilder 12.710 (± 7.9%) i/s - 64.000 in 5.073018s
simple_json 314.896 (± 3.2%) i/s - 1.600k in 5.086509s
jb 48.691 (± 2.1%) i/s (20.54 ms/i) - 244.000 in 5.013815s
jbuilder 27.930 (± 3.6%) i/s (35.80 ms/i) - 140.000 in 5.016897s
simple_json(oj) 29.083 (± 6.9%) i/s (34.38 ms/i) - 146.000 in 5.039076s
simple_json(AS::json)
99.792 (± 7.0%) i/s (10.02 ms/i) - 500.000 in 5.037716s

Comparison:
simple_json: 314.9 i/s
jb: 124.6 i/s - 2.53x (± 0.00) slower
jbuilder: 12.7 i/s - 24.78x (± 0.00) slower
simple_json(AS::json): 99.8 i/s
jb: 48.7 i/s - 2.05x slower
simple_json(oj): 29.1 i/s - 3.43x slower
jbuilder: 27.9 i/s - 3.57x slower
```

## Migrating from Jbuilder
Expand Down
9 changes: 9 additions & 0 deletions gemfiles/rails_8.1.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

source 'https://rubygems.org'

gemspec path: '../'

gem 'jbuilder'
gem 'rails', '~> 8.1.0'
gem 'selenium-webdriver'
2 changes: 1 addition & 1 deletion lib/simple_json/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module SimpleJson
VERSION = '0.1.0'
VERSION = '1.0.0'
end
2 changes: 1 addition & 1 deletion simple_json.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'action_args'
spec.add_development_dependency 'bundler'
spec.add_development_dependency 'debug'
spec.add_development_dependency 'rails', '~> 8.0'
spec.add_development_dependency 'rails', '>= 7.1'
spec.add_development_dependency 'rake'
spec.add_development_dependency 'selenium-webdriver'
spec.add_development_dependency 'test-unit-rails'
Expand Down
47 changes: 20 additions & 27 deletions test/dummy_app/app/controllers/benchmarks_controller.rb
Original file line number Diff line number Diff line change
@@ -1,37 +1,30 @@
# frozen_string_literal: true

require 'benchmark/ips'

class BenchmarksController < ApplicationController
def index(n = '100')
@comments = 1.upto(n.to_i).map { |i| Comment.new(i, nil, "comment #{i}") }
before_action :prepare_comments

def self.comments
@comments ||= 1.upto(1000).map { |i| Comment.new(i, nil, "comment #{i}") }
end

def jb
end

jb = render_to_string 'index_jb'
jbuilder = render_to_string 'index_jbuilder'
def jbuilder
end

def simple_json_oj
SimpleJson.json_module = SimpleJson::Json::Oj
simple_json = render_to_string 'index'
end

def simple_json_as_json
SimpleJson.json_module = ActiveSupport::JSON
simple_json_active_support_json = render_to_string 'index'

raise 'jb != jbuilder' unless jb == jbuilder
raise 'simple_json != jbuilder' unless simple_json == jbuilder
raise 'simple_json_active_support_json != jbuilder' unless simple_json_active_support_json == jbuilder

result = Benchmark.ips do |x|
x.report('jb') { render_to_string 'index_jb' }
x.report('jbuilder') { render_to_string 'index_jbuilder' }
x.report('simple_json(oj)') {
SimpleJson.json_module = SimpleJson::Json::Oj
render_to_string 'index'
}
x.report('simple_json(AS::json)') {
SimpleJson.json_module = ActiveSupport::JSON
render_to_string 'index'
}
x.compare!
end
render plain: result.data.to_s
end

private

def prepare_comments
n = params.fetch(:n, 100).to_i
@comments = self.class.comments.take(n)
end
end
54 changes: 53 additions & 1 deletion test/dummy_app/app/views/benchmarks/_comment_jb.json.jb
Original file line number Diff line number Diff line change
@@ -1,3 +1,55 @@
# frozen_string_literal: true

{ body: comment.body }
length = comment.body.length
likes = comment.id * 3
dislikes = comment.id % 4
rating = (likes.to_f / (dislikes.zero? ? 1 : dislikes)).round(2)
position = comment_counter + 1

{
id: comment.id,
body: comment.body,
metrics: {
length: length,
readability: (length / 5.0).round(2),
engagement: {
likes: likes,
dislikes: dislikes,
rating: rating
}
},
author: {
name: "user-#{comment.id}",
active: comment.id.odd?,
badges: (comment.id % 3).zero? ? %w[insightful] : [],
contact: {
email: "user#{comment.id}@example.com",
url: "https://example.com/users/#{comment.id}"
},
settings: {
theme: comment.id.even? ? 'dark' : 'light',
timezone: "UTC+#{(comment.id % 5) - 2}"
}
},
tags: ['bench', "comment-#{comment.id}", (comment.id.even? ? 'even' : nil)].compact,
flags: {
pinned: position == 1,
hidden: (comment.id % 5).zero?
},
metadata: {
position: position,
attachments: (comment.id % 4).zero? ? [{ filename: "attachment-#{comment.id}.txt", size: comment.id * 128 }] : [],
history: [
{ version: 1, updated_by: "user-#{comment.id}", updated_at: '2024-01-01T00:00:00Z' },
{ version: 2, updated_by: "user-#{comment.id}", updated_at: '2024-01-02T00:00:00Z' }
]
},
links: {
self: "/comments/#{comment.id}",
post: "/posts/#{comment.post&.id || 1}"
},
extras: {
mood: %w[happy neutral excited][comment.id % 3],
score: comment.id * position
}
}
Loading