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