diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index cab09e3..c8f8335 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -37,6 +37,26 @@ jobs:
- name: Run lint
run: bundle exec rubocop
+ benchmarks:
+ runs-on: ubuntu-latest
+ env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+ name: Benchmarks
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: "3.4"
+ bundler-cache: true
+ working-directory: benchmarks
+
+ - name: Run benchmarks
+ working-directory: benchmarks
+ run: bundle exec ruby benchmark.rb | tee -a "$GITHUB_STEP_SUMMARY"
+
# The release workflow seems to have some issues. I'll reinstate it when I have time to fix it.
# The failing check on `main` doesn't inspire confidence.
# In the meantime, I'll release manually.
diff --git a/.rubocop.yml b/.rubocop.yml
index ee61e2e..841cd02 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -3,10 +3,16 @@ plugins:
inherit_from: .rubocop_todo.yml
+inherit_mode:
+ merge:
+ - Exclude
+
AllCops:
TargetRubyVersion: 3.2
NewCops: disable
SuggestExtensions: false
+ Exclude:
+ - "benchmarks/**/*"
Style/StringLiterals:
Enabled: true
diff --git a/benchmarks/.rubocop.yml b/benchmarks/.rubocop.yml
new file mode 100644
index 0000000..d150456
--- /dev/null
+++ b/benchmarks/.rubocop.yml
@@ -0,0 +1,14 @@
+AllCops:
+ NewCops: disable
+ TargetRubyVersion: 3.2.5
+
+Style/StringLiterals:
+ Enabled: true
+ EnforcedStyle: double_quotes
+
+Style/StringLiteralsInInterpolation:
+ Enabled: true
+ EnforcedStyle: double_quotes
+
+Style/Documentation:
+ Enabled: false
diff --git a/benchmarks/Gemfile b/benchmarks/Gemfile
new file mode 100644
index 0000000..10ff58f
--- /dev/null
+++ b/benchmarks/Gemfile
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "pry"
+gem "rubocop"
+
+# Serializers
+gem "active_model_serializers"
+gem "alba"
+gem "jbuilder"
+gem "panko_serializer"
+gem "rabl"
+gem "representable"
+gem "transmutation", path: ".."
+
+# JSON generators
+gem "multi_json"
+gem "oj"
+
+# Benchmarking
+gem "benchmark-ips"
+gem "benchmark-memory"
+
+# CLI formatting
+gem "terminal-table"
+
+# Auto-loader
+gem "zeitwerk"
diff --git a/benchmarks/README.md b/benchmarks/README.md
new file mode 100644
index 0000000..63b0c8e
--- /dev/null
+++ b/benchmarks/README.md
@@ -0,0 +1,112 @@
+# Benchmarking popular Ruby JSON Serializers with Transmutation
+
+Transmutation provides a very simple and elegant DSL that outperforms the majority of other serializers on the market. It also boasts consistent performance with small standard deviation.
+
+\* _Benchmarks were ran on a MacBook Pro M1 Pro with the following Ruby version:_ `ruby 3.2.5 (2024-07-26 revision 31d0f1a2e7) +YJIT [arm64-darwin24]`
+
+The following objects are used for serialization:
+
+```ruby
+organisation = Organisation.new(id: 1, name: "Example Inc.") # Serialize with no associations
+user = User.new(id: 1, first_name: "John", last_name: "Doe", organisation_id: 1) # Serialize with many Posts
+post = Post.new(id: 1, title: "Sample Post", body: "Sample Body", user_id: 1) # Serialize with one User
+organisations = [organisation] + 29.times.map { Organisation.new(id: _1 + 2, name: "Example Inc. #{_1 + 2}") }
+```
+
+## Results
+
+### Attributes
+
+| Gem | IPS | Comparison | Allocations | Comparison |
+|-------------------------------------------------------------------------------------------|--------------------|--------------|-------------|------------|
+| [alba 3.6.0](https://github.com/okuramasafumi/alba) | 473.982k ± 1.6% | baseline | 1.128k | 1.11x more |
+| [panko_serializer 0.8.3](https://github.com/yosiat/panko_serializer) | 337.389k ±10.6% | 1.40x slower | 1.016k | baseline |
+| [transmutation 0.5.1](https://github.com/spellbook-technology/transmutation) | 314.398k ± 9.2% | 1.51x slower | 1.040k | 1.02x more |
+| [active_model_serializers 0.10.15](https://github.com/rails-api/active_model_serializers) | 202.110k ± 3.6% | 2.35x slower | 1.992k | 1.96x more |
+| [representable 3.2.0](https://github.com/trailblazer/representable/) | 157.150k ± 3.2% | 3.02x slower | 4.304k | 4.24x more |
+| [rabl 0.17.0](https://github.com/nesquena/rabl) | 105.862k ± 5.3% | 4.48x slower | 5.856k | 5.76x more |
+| [jbuilder 2.13.0](https://github.com/rails/jbuilder/tree/v2.13.0) | 88.011k ± 6.6% | 5.39x slower | 2.208k | 2.17x more |
+
+
+ JSON Output:
+
+ ```json
+ {"id":1,"name":"Example Inc.","logo_url":"https://example.com/logos/companies/1"}
+ ```
+
+
+### Has One / Belongs To
+
+| Gem | IPS | Comparison | Allocations | Comparison |
+|-------------------------------------------------------------------------------------------|--------------------|--------------|-------------|------------|
+| [alba 3.6.0](https://github.com/okuramasafumi/alba) | 214.622k ± 1.2% | baseline | 1.912k | baseline |
+| [transmutation 0.5.1](https://github.com/spellbook-technology/transmutation) | 168.840k ± 1.5% | 1.27x slower | 2.288k | 1.20x more |
+| [panko_serializer 0.8.3](https://github.com/yosiat/panko_serializer) | 127.328k ± 4.2% | 1.69x slower | 4.128k | 2.16x more |
+| [representable 3.2.0](https://github.com/trailblazer/representable/) | 81.974k ± 2.5% | 2.62x slower | 6.184k | 3.23x more |
+| [active_model_serializers 0.10.15](https://github.com/rails-api/active_model_serializers) | 62.949k ± 1.3% | 3.41x slower | 5.840k | 3.05x more |
+| [rabl 0.17.0](https://github.com/nesquena/rabl) | 57.561k ± 3.3% | 3.73x slower | 9.608k | 5.03x more |
+| [jbuilder 2.13.0](https://github.com/rails/jbuilder/tree/v2.13.0) | 43.080k ± 2.3% | 4.98x slower | 3.856k | 2.02x more |
+
+
+ JSON Output:
+
+ ```json
+ {"id":1,"title":"Sample Post","body":"Sample Body","user":{"id":1,"first_name":"John","full_name":"John Doe"}}
+ ```
+
+
+### Has Many
+
+| Gem | IPS | Comparison | Allocations | Comparison |
+|-------------------------------------------------------------------------------------------|--------------------|--------------|-------------|------------|
+| [panko_serializer 0.8.3](https://github.com/yosiat/panko_serializer) | 223.232k ± 2.5% | baseline | 1.376k | baseline |
+| [alba 3.6.0](https://github.com/okuramasafumi/alba) | 160.667k ± 1.1% | 1.39x slower | 2.440k | 1.77x more |
+| [transmutation 0.5.1](https://github.com/spellbook-technology/transmutation) | 124.635k ± 3.4% | 1.79x slower | 3.656k | 2.66x more |
+| [representable 3.2.0](https://github.com/trailblazer/representable/) | 53.158k ± 1.2% | 4.20x slower | 9.640k | 7.01x more |
+| [active_model_serializers 0.10.15](https://github.com/rails-api/active_model_serializers) | 43.965k ± 3.7% | 5.08x slower | 8.728k | 6.34x more |
+| [jbuilder 2.13.0](https://github.com/rails/jbuilder/tree/v2.13.0) | 43.513k ± 2.8% | 5.13x slower | 4.704k | 3.42x more |
+| [rabl 0.17.0](https://github.com/nesquena/rabl) | 28.359k ± 1.2% | 7.87x slower | 10.912k | 7.93x more |
+
+
+ JSON Output:
+
+ ```json
+ {"id":1,"first_name":"John","full_name":"John Doe","posts":[{"id":1,"title":"Post 1","body":"Sample body 1"},{"id":3,"title":"Post 3","body":"Sample body 3"}]}
+ ```
+
+
+### Collection
+
+| Gem | IPS | Comparison | Allocations | Comparison |
+|-------------------------------------------------------------------------------------------|--------------------|---------------|-------------|-------------|
+| [panko_serializer 0.8.3](https://github.com/yosiat/panko_serializer) | 69.118k ± 1.1% | baseline | 7.616k | baseline |
+| [alba 3.6.0](https://github.com/okuramasafumi/alba) | 26.113k ± 1.2% | 2.65x slower | 15.911k | 2.09x more |
+| [transmutation 0.5.1](https://github.com/spellbook-technology/transmutation) | 20.735k ± 1.2% | 3.33x slower | 28.183k | 3.70x more |
+| [rabl 0.17.0](https://github.com/nesquena/rabl) | 11.969k ± 1.1% | 5.77x slower | 21.527k | 2.83x more |
+| [active_model_serializers 0.10.15](https://github.com/rails-api/active_model_serializers) | 7.514k ± 3.3% | 9.20x slower | 69.927k | 9.18x more |
+| [jbuilder 2.13.0](https://github.com/rails/jbuilder/tree/v2.13.0) | 5.589k ±18.9% | 12.37x slower | 32.662k | 4.29x more |
+| [representable 3.2.0](https://github.com/trailblazer/representable/) | 4.953k ± 1.0% | 13.96x slower | 160.543k | 21.08x more |
+
+
+ JSON Output:
+
+ ```json
+ [{"id":1,"name":"Example Inc.","logo_url":"https://example.com/logos/companies/1"},{"id":2,"name":"Example Inc. 2","logo_url":"https://example.com/logos/companies/2"},{"id":3,"name":"Example Inc. 3","logo_url":"https://example.com/logos/companies/3"},{"id":4,"name":"Example Inc. 4","logo_url":"https://example.com/logos/companies/4"},{"id":5,"name":"Example Inc. 5","logo_url":"https://example.com/logos/companies/5"},{"id":6,"name":"Example Inc. 6","logo_url":"https://example.com/logos/companies/6"},{"id":7,"name":"Example Inc. 7","logo_url":"https://example.com/logos/companies/7"},{"id":8,"name":"Example Inc. 8","logo_url":"https://example.com/logos/companies/8"},{"id":9,"name":"Example Inc. 9","logo_url":"https://example.com/logos/companies/9"},{"id":10,"name":"Example Inc. 10","logo_url":"https://example.com/logos/companies/10"},{"id":11,"name":"Example Inc. 11","logo_url":"https://example.com/logos/companies/11"},{"id":12,"name":"Example Inc. 12","logo_url":"https://example.com/logos/companies/12"},{"id":13,"name":"Example Inc. 13","logo_url":"https://example.com/logos/companies/13"},{"id":14,"name":"Example Inc. 14","logo_url":"https://example.com/logos/companies/14"},{"id":15,"name":"Example Inc. 15","logo_url":"https://example.com/logos/companies/15"},{"id":16,"name":"Example Inc. 16","logo_url":"https://example.com/logos/companies/16"},{"id":17,"name":"Example Inc. 17","logo_url":"https://example.com/logos/companies/17"},{"id":18,"name":"Example Inc. 18","logo_url":"https://example.com/logos/companies/18"},{"id":19,"name":"Example Inc. 19","logo_url":"https://example.com/logos/companies/19"},{"id":20,"name":"Example Inc. 20","logo_url":"https://example.com/logos/companies/20"},{"id":21,"name":"Example Inc. 21","logo_url":"https://example.com/logos/companies/21"},{"id":22,"name":"Example Inc. 22","logo_url":"https://example.com/logos/companies/22"},{"id":23,"name":"Example Inc. 23","logo_url":"https://example.com/logos/companies/23"},{"id":24,"name":"Example Inc. 24","logo_url":"https://example.com/logos/companies/24"},{"id":25,"name":"Example Inc. 25","logo_url":"https://example.com/logos/companies/25"},{"id":26,"name":"Example Inc. 26","logo_url":"https://example.com/logos/companies/26"},{"id":27,"name":"Example Inc. 27","logo_url":"https://example.com/logos/companies/27"},{"id":28,"name":"Example Inc. 28","logo_url":"https://example.com/logos/companies/28"},{"id":29,"name":"Example Inc. 29","logo_url":"https://example.com/logos/companies/29"},{"id":30,"name":"Example Inc. 30","logo_url":"https://example.com/logos/companies/30"}]
+ ```
+
+
+## How to run the benchmarks yourself
+
+In order to run the benchmarks, you'll need to have Ruby and Bundler installed on your system. Once you've installed all the dependencies with `bundle install`, you can run the benchmarks with or without YJIT enabled.
+
+- Run the benchmarks without YJIT enabled
+
+ ```sh
+ bundle exec ruby benchmark.rb
+ ```
+
+- Run the benchmarks with YJIT enabled
+
+ ```sh
+ bundle exec ruby --yjit benchmark.rb
+ ```
diff --git a/benchmarks/benchmark.rb b/benchmarks/benchmark.rb
new file mode 100644
index 0000000..23c4a2d
--- /dev/null
+++ b/benchmarks/benchmark.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "active_support/core_ext/object/deep_dup" # Required for Active Model Serializers
+
+Bundler.require
+
+Oj.optimize_rails # Use OJ for benchmarks using #to_json
+MultiJson.use(:oj) # Use OJ by default from multi_json
+
+loader = Zeitwerk::Loader.new
+loader.push_dir(File.expand_path("lib", __dir__))
+loader.collapse(File.expand_path("lib/models", __dir__))
+loader.collapse(File.expand_path("lib/serializers", __dir__))
+loader.setup
+
+Rabl.configure do |config|
+ config.include_json_root = false
+ config.include_child_root = false
+end
+
+user_jbuilder_template = File.read(File.expand_path("lib/views/users/show.json.jbuilder", __dir__))
+post_jbuilder_template = File.read(File.expand_path("lib/views/posts/show.json.jbuilder", __dir__))
+organisation_jbuilder_template = File.read(File.expand_path("lib/views/organisations/show.json.jbuilder", __dir__))
+organisations_jbuilder_template = File.read(File.expand_path("lib/views/organisations/index.json.jbuilder", __dir__))
+
+user_rabl_template = File.read(File.expand_path("lib/views/users/show.json.rabl", __dir__))
+post_rabl_template = File.read(File.expand_path("lib/views/posts/show.json.rabl", __dir__))
+organisation_rabl_template = File.read(File.expand_path("lib/views/organisations/show.json.rabl", __dir__))
+organisations_rabl_template = File.read(File.expand_path("lib/views/organisations/index.json.rabl", __dir__))
+
+organisation = Organisation.new(id: 1, name: "Example Inc.")
+user = User.new(id: 1, first_name: "John", last_name: "Doe", organisation_id: 1)
+post = Post.new(id: 1, title: "Sample Post", body: "Sample Body", user_id: 1)
+
+organisations = [organisation] + 29.times.map { Organisation.new(id: _1 + 2, name: "Example Inc. #{_1 + 2}") }
+
+GemBenchmarks.report output: false do
+ group("Attributes") do
+ example("transmutation") { Transmutation::OrganisationSerializer.new(organisation).to_json }
+ example("panko_serializer") { PankoSerializer::OrganisationSerializer.new.serialize_to_json(organisation) }
+ example("jbuilder") { Jbuilder.encode { |json| json.instance_eval(organisation_jbuilder_template); json.target! } }
+ example("representable") { Representable::OrganisationRepresenter.new(organisation).to_json }
+ example("active_model_serializers") { ActiveModelSerializers::OrganisationSerializer.new(organisation, namespace: ActiveModelSerializers).to_json }
+ example("rabl") { Rabl::Renderer.json(organisation, organisation_rabl_template) }
+ example("alba") { Alba::OrganisationResource.new(organisation).to_json }
+ end
+
+ group("Has One / Belongs To") do
+ example("transmutation") { Transmutation::PostSerializer.new(post).to_json }
+ example("panko_serializer") { PankoSerializer::PostSerializer.new(except: { user: [:posts] }).serialize_to_json(post) }
+ example("jbuilder") { Jbuilder.encode { |json| json.instance_eval(post_jbuilder_template); json.target! } }
+ example("representable") { Representable::PostRepresenter.new(post).to_json }
+ example("active_model_serializers") { ActiveModelSerializers::PostSerializer.new(post, namespace: ActiveModelSerializers).to_json }
+ example("rabl") { Rabl::Renderer.json(post, post_rabl_template) }
+ example("alba") { Alba::PostResource.new(post, within: :user).to_json }
+ end
+
+ group("Has Many") do
+ example("transmutation") { Transmutation::UserSerializer.new(user).to_json }
+ example("panko_serializer") { PankoSerializer::UserSerializer.new.serialize_to_json(user) }
+ example("jbuilder") { Jbuilder.encode { |json| json.instance_eval(user_jbuilder_template); json.target! } }
+ example("representable") { Representable::UserRepresenter.new(user).to_json }
+ example("active_model_serializers") { ActiveModelSerializers::UserSerializer.new(user, namespace: ActiveModelSerializers).to_json }
+ example("rabl") { Rabl::Renderer.json(user, user_rabl_template) }
+ example("alba") { Alba::UserResource.new(user, within: :posts).to_json }
+ end
+
+ group("Collection") do
+ example("transmutation") { organisations.map { Transmutation::OrganisationSerializer.new(_1) }.to_json }
+ example("panko_serializer") { Panko::ArraySerializer.new(organisations, each_serializer: PankoSerializer::OrganisationSerializer).to_json }
+ example("jbuilder") { Jbuilder.encode { |json| json.instance_eval(organisations_jbuilder_template); json.target! } }
+ example("representable") { Representable::OrganisationRepresenter.for_collection.new(organisations).to_json }
+ example("active_model_serializers") { ActiveModel::Serializer::CollectionSerializer.new(organisations, each_serializer: ActiveModelSerializers::OrganisationSerializer, namespace: ActiveModelSerializers).to_json }
+ example("rabl") { Rabl::Renderer.json(organisations, organisations_rabl_template) }
+ example("alba") { Alba::OrganisationResource.new(organisations, within: :posts).to_json }
+ end
+end
diff --git a/benchmarks/lib/gem_benchmarks.rb b/benchmarks/lib/gem_benchmarks.rb
new file mode 100644
index 0000000..2361fb3
--- /dev/null
+++ b/benchmarks/lib/gem_benchmarks.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+class GemBenchmarks
+ def self.report(**options, &block)
+ new(**options, &block).groups.each_value(&:report)
+ end
+
+ def initialize(**options, &block)
+ config[:output] = options.fetch(:output, true)
+ config[:style] = options.fetch(:style, :markdown)
+
+ instance_eval(&block)
+ end
+
+ def groups
+ @groups ||= {}
+ end
+
+ def config
+ @config ||= {}
+ end
+
+ private
+
+ def group(name, &block)
+ groups[name] = Group.new(name, **config, &block)
+ end
+
+ class Group
+ attr_accessor :name, :config
+
+ def initialize(name, **config, &block)
+ @name = name
+ @config = config
+
+ instance_eval(&block)
+ end
+
+ def calculate_outputs
+ examples.each_value do |example|
+ example.output = example.run
+ end
+ end
+
+ def calculate_time
+ time_report = Benchmark.ips quiet: true do |x|
+ examples.each do |name, example|
+ x.report(name) { example.run }
+ end
+ end
+
+ fastest_entry = time_report.entries.max_by(&:ips)
+
+ time_report.entries.each do |entry|
+ examples[entry.label].ips = "#{Benchmark::IPS::Helpers.scale(entry.ips)} #{format("±%4.1f%%", (100.0 * entry.ips_sd.to_f / entry.ips))}"
+ examples[entry.label].ips_comparison = fastest_entry.ips != entry.ips ? "#{format("%.2fx slower", fastest_entry.ips.to_f / entry.ips)}" : "baseline"
+ end
+
+ time_report
+ end
+
+ def calculate_memory
+ memory_report = Benchmark.memory quiet: true do |x|
+ examples.each do |name, example|
+ x.report(name) { example.run }
+ end
+ end
+
+ smallest_entry = memory_report.entries.min_by { |entry| entry.measurement.memory.allocated }
+
+ memory_report.entries.each do |entry|
+ examples[entry.label].allocations = Benchmark::Memory::Helpers.scale(entry.measurement.memory.allocated)
+ examples[entry.label].allocations_comparison = smallest_entry.measurement.memory.allocated != entry.measurement.memory.allocated ? "#{format("%.2fx more", entry.measurement.memory.allocated.to_f / smallest_entry.measurement.memory.allocated)}" : "baseline"
+ end
+
+ memory_report
+ end
+
+ def calculate
+ calculate_outputs if config[:output]
+ calculate_time
+ calculate_memory
+
+ nil
+ end
+
+ def report
+ calculate
+
+ table = Terminal::Table.new(headings:, rows:, style: { border: config[:style] })
+
+ table.align_column 1, :right
+ table.align_column 2, :right
+ table.align_column 3, :right
+ table.align_column 4, :right
+
+ puts "### #{name}\n\n#{table}\n\n"
+ end
+
+ def examples
+ @examples ||= {}
+ end
+
+ def headings
+ ["Gem", "IPS", "Comparison", "Allocations", "Comparison", (config[:output] ? "Output" : nil)].compact
+ end
+
+ def rows
+ examples.values.lazy.sort_by(&:ips).reverse.map do |example|
+ [example.label, example.ips, example.ips_comparison, example.allocations, example.allocations_comparison, (config[:output] ? example.output : nil)].compact
+ end
+ end
+
+ private
+
+ def example(gem_name, &block)
+ examples[gem_name] = Example.new(gem_name, **config, &block)
+ end
+
+ class Example
+ attr_accessor :label, :block, :ips, :ips_comparison, :allocations, :allocations_comparison, :output
+ attr_accessor :gem_name, :config
+
+ def initialize(gem_name, **config, &block)
+ @gem_name = gem_name
+ @config = config
+ @label = if config[:style] == :markdown
+ "[#{gem_name} #{gem_version}](#{gem_url})"
+ else
+ "#{gem_name} #{gem_version}"
+ end
+ @block = block
+ end
+
+ def run
+ block.call
+ end
+
+ private
+
+ def gem_version
+ specs.version
+ end
+
+ def gem_url
+ specs.metadata["source_code_uri"] || specs.homepage || "https://rubygems.org/gems/#{gem_name}"
+ end
+
+ def specs
+ Gem.loaded_specs[gem_name]
+ end
+ end
+ end
+end
diff --git a/benchmarks/lib/models/base.rb b/benchmarks/lib/models/base.rb
new file mode 100644
index 0000000..839095a
--- /dev/null
+++ b/benchmarks/lib/models/base.rb
@@ -0,0 +1,11 @@
+class Base
+ def initialize(**attributes)
+ attributes.each do |key, value|
+ send("#{key}=", value)
+ end
+ end
+
+ def read_attribute_for_serialization(attribute)
+ send(attribute)
+ end
+end
diff --git a/benchmarks/lib/models/organisation.rb b/benchmarks/lib/models/organisation.rb
new file mode 100644
index 0000000..dc73d3f
--- /dev/null
+++ b/benchmarks/lib/models/organisation.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Organisation < Base
+ attr_accessor :id, :name
+
+ def self.all
+ @all ||= [
+ Organisation.new(id: 1, name: "Organisation 1"),
+ ]
+ end
+end
diff --git a/benchmarks/lib/models/post.rb b/benchmarks/lib/models/post.rb
new file mode 100644
index 0000000..2daa09c
--- /dev/null
+++ b/benchmarks/lib/models/post.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class Post < Base
+ attr_accessor :id, :title, :body, :user_id
+
+ def self.all
+ @all ||= [
+ Post.new(id: 1, title: "Post 1", body: "Sample body 1", user_id: 1),
+ Post.new(id: 2, title: "Post 2", body: "Sample body 2", user_id: 2),
+ Post.new(id: 3, title: "Post 3", body: "Sample body 3", user_id: 1)
+ ]
+ end
+
+ def user
+ User.all.find { |user| user.id == user_id }
+ end
+end
diff --git a/benchmarks/lib/models/user.rb b/benchmarks/lib/models/user.rb
new file mode 100644
index 0000000..8d246ca
--- /dev/null
+++ b/benchmarks/lib/models/user.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class User < Base
+ attr_accessor :id, :first_name, :last_name, :organisation_id
+
+ def self.all
+ @all ||= [
+ User.new(id: 1, first_name: "John", last_name: "Doe", organisation_id: 1),
+ ]
+ end
+
+ def organisation
+ Organisation.all.find { |org| org.id == organisation_id }
+ end
+
+ def posts
+ Post.all.find_all { |post| post.user_id == id }
+ end
+
+ def post_ids
+ posts.map(&:id)
+ end
+end
diff --git a/benchmarks/lib/serializers/active_model_serializers/organisation_serializer.rb b/benchmarks/lib/serializers/active_model_serializers/organisation_serializer.rb
new file mode 100644
index 0000000..ccb28ba
--- /dev/null
+++ b/benchmarks/lib/serializers/active_model_serializers/organisation_serializer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module ActiveModelSerializers
+ class OrganisationSerializer < ActiveModel::Serializer
+ attributes :id, :name
+
+ attribute :logo_url do
+ "https://example.com/logos/companies/#{object.id}"
+ end
+ end
+end
diff --git a/benchmarks/lib/serializers/active_model_serializers/post_serializer.rb b/benchmarks/lib/serializers/active_model_serializers/post_serializer.rb
new file mode 100644
index 0000000..00601c3
--- /dev/null
+++ b/benchmarks/lib/serializers/active_model_serializers/post_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ActiveModelSerializers
+ class PostSerializer < ActiveModel::Serializer
+ attributes :id, :title, :body
+
+ belongs_to :user
+ end
+end
diff --git a/benchmarks/lib/serializers/active_model_serializers/user_serializer.rb b/benchmarks/lib/serializers/active_model_serializers/user_serializer.rb
new file mode 100644
index 0000000..135f48a
--- /dev/null
+++ b/benchmarks/lib/serializers/active_model_serializers/user_serializer.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActiveModelSerializers
+ class UserSerializer < ActiveModel::Serializer
+ attributes :id, :first_name
+
+ attribute :full_name do
+ "#{object.first_name} #{object.last_name}"
+ end
+
+ has_many :posts
+ end
+end
diff --git a/benchmarks/lib/serializers/alba/organisation_resource.rb b/benchmarks/lib/serializers/alba/organisation_resource.rb
new file mode 100644
index 0000000..a3e9a99
--- /dev/null
+++ b/benchmarks/lib/serializers/alba/organisation_resource.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Alba
+ class OrganisationResource
+ include Alba::Resource
+
+ attributes :id, :name
+
+ attribute :logo_url do |resource|
+ "https://example.com/logos/companies/#{resource.id}"
+ end
+ end
+end
diff --git a/benchmarks/lib/serializers/alba/post_resource.rb b/benchmarks/lib/serializers/alba/post_resource.rb
new file mode 100644
index 0000000..848683e
--- /dev/null
+++ b/benchmarks/lib/serializers/alba/post_resource.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Alba
+ class PostResource
+ include Alba::Resource
+
+ attributes :id, :title, :body
+
+ one :user, resource: UserResource
+ end
+end
diff --git a/benchmarks/lib/serializers/alba/user_resource.rb b/benchmarks/lib/serializers/alba/user_resource.rb
new file mode 100644
index 0000000..91e42bc
--- /dev/null
+++ b/benchmarks/lib/serializers/alba/user_resource.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Alba
+ class UserResource
+ include Alba::Resource
+
+ attributes :id, :first_name
+
+ attribute :full_name do |resource|
+ "#{resource.first_name} #{resource.last_name}"
+ end
+
+ many :posts, resource: PostResource
+ end
+end
diff --git a/benchmarks/lib/serializers/panko_serializer/organisation_serializer.rb b/benchmarks/lib/serializers/panko_serializer/organisation_serializer.rb
new file mode 100644
index 0000000..f3ce562
--- /dev/null
+++ b/benchmarks/lib/serializers/panko_serializer/organisation_serializer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module PankoSerializer
+ class OrganisationSerializer < Panko::Serializer
+ attributes :id, :name, :logo_url
+
+ def logo_url
+ "https://example.com/logos/companies/#{object.id}"
+ end
+ end
+end
diff --git a/benchmarks/lib/serializers/panko_serializer/post_serializer.rb b/benchmarks/lib/serializers/panko_serializer/post_serializer.rb
new file mode 100644
index 0000000..5eb48f8
--- /dev/null
+++ b/benchmarks/lib/serializers/panko_serializer/post_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module PankoSerializer
+ class PostSerializer < Panko::Serializer
+ attributes :id, :title, :body
+
+ has_one :user
+ end
+end
diff --git a/benchmarks/lib/serializers/panko_serializer/user_serializer.rb b/benchmarks/lib/serializers/panko_serializer/user_serializer.rb
new file mode 100644
index 0000000..dcd53e5
--- /dev/null
+++ b/benchmarks/lib/serializers/panko_serializer/user_serializer.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module PankoSerializer
+ class UserSerializer < Panko::Serializer
+ attributes :id, :first_name, :full_name
+
+ def full_name
+ "#{object.first_name} #{object.last_name}"
+ end
+
+ has_many :posts
+ end
+end
diff --git a/benchmarks/lib/serializers/representable/organisation_representer.rb b/benchmarks/lib/serializers/representable/organisation_representer.rb
new file mode 100644
index 0000000..e1711b1
--- /dev/null
+++ b/benchmarks/lib/serializers/representable/organisation_representer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Representable
+ class OrganisationRepresenter < Representable::Decorator
+ include Representable::JSON
+
+ property :id
+ property :name
+ property :logo_url, getter: ->(represented:, **) { "https://example.com/logos/companies/#{represented.id}" }
+ end
+end
diff --git a/benchmarks/lib/serializers/representable/post_representer.rb b/benchmarks/lib/serializers/representable/post_representer.rb
new file mode 100644
index 0000000..35d5d93
--- /dev/null
+++ b/benchmarks/lib/serializers/representable/post_representer.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Representable
+ class PostRepresenter < Representable::Decorator
+ include Representable::JSON
+
+ property :id
+ property :title
+ property :body
+
+ property :user do
+ property :id
+ property :first_name
+ property :full_name, getter: ->(represented:, **) { "#{represented.first_name} #{represented.last_name}" }
+ end
+ end
+end
diff --git a/benchmarks/lib/serializers/representable/user_representer.rb b/benchmarks/lib/serializers/representable/user_representer.rb
new file mode 100644
index 0000000..7d89d3f
--- /dev/null
+++ b/benchmarks/lib/serializers/representable/user_representer.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Representable
+ class UserRepresenter < Representable::Decorator
+ include Representable::JSON
+
+ property :id
+ property :first_name
+ property :full_name, getter: ->(represented:, **) { "#{represented.first_name} #{represented.last_name}" }
+
+ collection :posts do
+ property :id
+ property :title
+ property :body
+ end
+ end
+end
diff --git a/benchmarks/lib/serializers/transmutation/organisation_serializer.rb b/benchmarks/lib/serializers/transmutation/organisation_serializer.rb
new file mode 100644
index 0000000..d41f906
--- /dev/null
+++ b/benchmarks/lib/serializers/transmutation/organisation_serializer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Transmutation
+ class OrganisationSerializer < Transmutation::Serializer
+ attributes :id, :name
+
+ attribute :logo_url do
+ "https://example.com/logos/companies/#{object.id}"
+ end
+ end
+end
diff --git a/benchmarks/lib/serializers/transmutation/post_serializer.rb b/benchmarks/lib/serializers/transmutation/post_serializer.rb
new file mode 100644
index 0000000..e3c9112
--- /dev/null
+++ b/benchmarks/lib/serializers/transmutation/post_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Transmutation
+ class PostSerializer < Transmutation::Serializer
+ attributes :id, :title, :body
+
+ belongs_to :user
+ end
+end
diff --git a/benchmarks/lib/serializers/transmutation/user_serializer.rb b/benchmarks/lib/serializers/transmutation/user_serializer.rb
new file mode 100644
index 0000000..fbbe6cf
--- /dev/null
+++ b/benchmarks/lib/serializers/transmutation/user_serializer.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Transmutation
+ class UserSerializer < Transmutation::Serializer
+ attributes :id, :first_name
+
+ attribute :full_name do
+ "#{object.first_name} #{object.last_name}"
+ end
+
+ has_many :posts
+ end
+end
diff --git a/benchmarks/lib/views/organisations/index.json.jbuilder b/benchmarks/lib/views/organisations/index.json.jbuilder
new file mode 100644
index 0000000..c622eb4
--- /dev/null
+++ b/benchmarks/lib/views/organisations/index.json.jbuilder
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+json.array! organisations do |organisation|
+ json.call(organisation, :id, :name)
+ json.logo_url "https://example.com/logos/companies/#{organisation.id}"
+end
diff --git a/benchmarks/lib/views/organisations/index.json.rabl b/benchmarks/lib/views/organisations/index.json.rabl
new file mode 100644
index 0000000..0e1691e
--- /dev/null
+++ b/benchmarks/lib/views/organisations/index.json.rabl
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+collection @organisations
+attributes :id, :name
+node(:logo_url) { |organisation| "https://example.com/logos/companies/#{organisation.id}" }
diff --git a/benchmarks/lib/views/organisations/show.json.jbuilder b/benchmarks/lib/views/organisations/show.json.jbuilder
new file mode 100644
index 0000000..8513fbc
--- /dev/null
+++ b/benchmarks/lib/views/organisations/show.json.jbuilder
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+json.call(organisation, :id, :name)
+json.logo_url "https://example.com/logos/companies/#{organisation.id}"
diff --git a/benchmarks/lib/views/organisations/show.json.rabl b/benchmarks/lib/views/organisations/show.json.rabl
new file mode 100644
index 0000000..e7b90a3
--- /dev/null
+++ b/benchmarks/lib/views/organisations/show.json.rabl
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+object @organisation
+attributes :id, :name
+node(:logo_url) { |organisation| "https://example.com/logos/companies/#{organisation.id}" }
diff --git a/benchmarks/lib/views/posts/show.json.jbuilder b/benchmarks/lib/views/posts/show.json.jbuilder
new file mode 100644
index 0000000..549cb9f
--- /dev/null
+++ b/benchmarks/lib/views/posts/show.json.jbuilder
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+json.call(post, :id, :title, :body)
+json.user do
+ json.id post.user.id
+ json.first_name post.user.first_name
+ json.full_name "#{post.user.first_name} #{post.user.last_name}"
+end
diff --git a/benchmarks/lib/views/posts/show.json.rabl b/benchmarks/lib/views/posts/show.json.rabl
new file mode 100644
index 0000000..aff15f8
--- /dev/null
+++ b/benchmarks/lib/views/posts/show.json.rabl
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+object @post
+attributes :id, :title, :body
+child(:user) do
+ attributes :id, :first_name
+ node(:full_name) { |user| "#{user.first_name} #{user.last_name}" }
+end
diff --git a/benchmarks/lib/views/users/show.json.jbuilder b/benchmarks/lib/views/users/show.json.jbuilder
new file mode 100644
index 0000000..1b1d104
--- /dev/null
+++ b/benchmarks/lib/views/users/show.json.jbuilder
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+json.call(user, :id, :first_name)
+json.full_name "#{user.first_name} #{user.last_name}"
+json.posts user.posts do |post|
+ json.call(post, :id, :title, :body)
+end
diff --git a/benchmarks/lib/views/users/show.json.rabl b/benchmarks/lib/views/users/show.json.rabl
new file mode 100644
index 0000000..554fbb4
--- /dev/null
+++ b/benchmarks/lib/views/users/show.json.rabl
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+object @user
+attributes :id, :first_name
+node(:full_name) { |user| "#{user.first_name} #{user.last_name}" }
+child(:posts) do
+ attributes :id, :title, :body
+end