├── .clang-format ├── .github ├── dependabot.yml └── workflows │ ├── docs.yml │ ├── lint.yml │ └── ruby.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── Appraisals ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── benchmarks ├── BENCHMARKS.md ├── allocs.rb ├── app.rb ├── benchmarking_support.rb ├── bm_ams_0_10.rb ├── bm_object_writer.rb ├── bm_panko_json.rb ├── bm_panko_object.rb ├── bm_plain_object.rb ├── bm_serialization_descriptor.rb ├── bm_serializer_resolver.rb ├── bm_to_object.rb ├── profile.rb ├── sanity.rb ├── setup.rb └── type_casts │ ├── bm_active_record.rb │ ├── bm_panko.rb │ └── support.rb ├── docs ├── docs │ ├── associations.md │ ├── attributes.md │ ├── design-choices.md │ ├── getting-started.md │ ├── introduction.md │ ├── performance.md │ └── response-bag.md ├── docusaurus.config.js ├── package-lock.json ├── package.json ├── sidebars.json ├── src │ └── css │ │ └── customTheme.css └── static │ ├── .DS_Store │ ├── CNAME │ ├── css │ └── custom.css │ ├── img │ ├── favicon.ico │ ├── oss_logo.png │ ├── undraw_code_review.svg │ ├── undraw_monitor.svg │ ├── undraw_note_list.svg │ ├── undraw_online.svg │ ├── undraw_open_source.svg │ ├── undraw_operating_system.svg │ ├── undraw_react.svg │ ├── undraw_tweetstorm.svg │ └── undraw_youtube_tutorial.svg │ └── index.html ├── ext └── panko_serializer │ ├── attributes_writer │ ├── active_record.c │ ├── active_record.h │ ├── attributes_writer.c │ ├── attributes_writer.h │ ├── common.c │ ├── common.h │ ├── hash.c │ ├── hash.h │ ├── plain.c │ ├── plain.h │ └── type_cast │ │ ├── time_conversion.c │ │ ├── time_conversion.h │ │ ├── type_cast.c │ │ └── type_cast.h │ ├── common.h │ ├── extconf.rb │ ├── panko_serializer.c │ ├── panko_serializer.h │ └── serialization_descriptor │ ├── association.c │ ├── association.h │ ├── attribute.c │ ├── attribute.h │ ├── serialization_descriptor.c │ └── serialization_descriptor.h ├── gemfiles ├── 7.0.0.gemfile ├── 7.0.0.gemfile.lock ├── 7.1.0.gemfile ├── 7.1.0.gemfile.lock ├── 7.2.0.gemfile ├── 7.2.0.gemfile.lock ├── 8.0.0.gemfile └── 8.0.0.gemfile.lock ├── lib ├── panko │ ├── array_serializer.rb │ ├── association.rb │ ├── attribute.rb │ ├── object_writer.rb │ ├── response.rb │ ├── serialization_descriptor.rb │ ├── serializer.rb │ ├── serializer_resolver.rb │ └── version.rb └── panko_serializer.rb ├── panko_serializer.gemspec └── spec ├── models.rb ├── panko ├── array_serializer_spec.rb ├── object_writer_spec.rb ├── response_spec.rb ├── serialization_descriptor_spec.rb ├── serializer_resolver_spec.rb ├── serializer_spec.rb └── type_cast_spec.rb └── spec_helper.rb /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: Google 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Docs Publishing 5 | 6 | on: 7 | push: 8 | branches: [ master, docup ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | env: 15 | working_directory: 'docs/' 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: '20.x' 23 | 24 | - name: Install Dependencies 25 | working-directory: ${{ env.working_directory }} 26 | run: npm install 27 | 28 | - name: Install SSH Client 🔑 29 | uses: webfactory/ssh-agent@v0.6.0 30 | with: 31 | ssh-private-key: ${{ secrets.DEPLOY_KEY }} 32 | 33 | - name: Publish 34 | working-directory: ${{ env.working_directory }} 35 | run: | 36 | git config --global user.email "action@github.com" 37 | git config --global user.name "GitHub Action" 38 | GIT_USER=yosiat CURRENT_BRANCH=master USE_SSH=true npm run deploy 39 | 40 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Install deps 13 | run: | 14 | sudo apt update -y 15 | sudo apt install -y libsqlite3-dev 16 | 17 | - name: Lint Ruby code 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: 3 21 | bundler-cache: true 22 | - run: | 23 | bundle exec rake rubocop 24 | 25 | - name: Lint C 26 | uses: jidicula/clang-format-action@v4.15.0 27 | with: 28 | clang-format-version: "16" 29 | check-path: "ext/panko_serializer" 30 | fallback-style: "Google" 31 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Panko Serializer CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | ruby: ["3.1", "3.2", "3.3", "3.4"] 12 | rails: ["7.0.0", "7.1.0", "7.2.0", "8.0.0"] 13 | exclude: 14 | - ruby: 3.1 15 | rails: 8.0.0 16 | - ruby: 3.4 17 | rails: 7.0.0 18 | 19 | steps: 20 | - name: Install deps 21 | run: | 22 | sudo apt update -y 23 | sudo apt install -y libsqlite3-dev 24 | 25 | - uses: actions/checkout@v4 26 | - name: Set up Ruby ${{ matrix.ruby }} 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{ matrix.ruby }} 30 | 31 | 32 | - name: Gems Cache 33 | id: gem-cache 34 | uses: actions/cache@v4 35 | with: 36 | path: vendor/bundle 37 | key: ${{ runner.os }}-${{ matrix.ruby }}-${{ matrix.rails }}-gem 38 | restore-keys: | 39 | ${{ runner.os }}-${{ matrix.ruby }}-${{ matrix.rails }}-gem 40 | 41 | - name: Install gems 42 | env: 43 | BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile 44 | run: | 45 | gem install bundler 46 | bundle config set path 'vendor/bundle' 47 | bundle check || bundle install --jobs 4 --retry 3 48 | 49 | - name: Compile & test 50 | env: 51 | BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile 52 | run: | 53 | bundle exec rake 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /vendor/bundle/ 11 | .byebug_history 12 | *.bundle 13 | 14 | # rspec failure tracking 15 | .rspec_status 16 | node_modules 17 | 18 | /docs/build/ 19 | .cache/ 20 | .docusaurus/ 21 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # We want Exclude directives from different 2 | # config files to get merged, not overwritten 3 | inherit_mode: 4 | merge: 5 | - Exclude 6 | 7 | require: 8 | - standard 9 | - rubocop-performance 10 | - standard-performance 11 | - rubocop-rspec 12 | 13 | inherit_gem: 14 | standard: config/base.yml 15 | standard-performance: config/base.yml 16 | 17 | AllCops: 18 | TargetRubyVersion: 3.1 19 | SuggestExtensions: false 20 | NewCops: disable 21 | Exclude: 22 | - ext/**/* 23 | - gemfiles/**/* 24 | 25 | 26 | Style/FrozenStringLiteralComment: 27 | Enabled: true 28 | EnforcedStyle: always 29 | SafeAutoCorrect: true 30 | 31 | # TODO: need to work on specs. 32 | RSpec: 33 | Enabled: false 34 | 35 | Lint/ConstantDefinitionInBlock: 36 | Exclude: 37 | - spec/**/* -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise "7.0.0" do 4 | gem "sqlite3", "~> 1.4" 5 | gem "activesupport", "~> 7.0.0" 6 | gem "activemodel", "~> 7.0.0" 7 | gem "activerecord", "~> 7.0.0", group: :test 8 | end 9 | 10 | appraise "7.1.0" do 11 | gem "sqlite3", "~> 1.4" 12 | gem "activesupport", "~> 7.1.5" 13 | gem "activemodel", "~> 7.1.5" 14 | gem "activerecord", "~> 7.1.5", group: :test 15 | end 16 | 17 | appraise "7.2.0" do 18 | gem "sqlite3", "~> 1.4" 19 | gem "activesupport", "~> 7.2.0" 20 | gem "activemodel", "~> 7.2.0" 21 | gem "activerecord", "~> 7.2.0", group: :test 22 | end 23 | 24 | appraise "8.0.0" do 25 | gem "sqlite3", ">= 2.1" 26 | gem "activesupport", "~> 8.0.0" 27 | gem "activemodel", "~> 8.0.0" 28 | gem "activerecord", "~> 8.0.0", group: :test 29 | end 30 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | 7 | group :benchmarks do 8 | gem "vernier" 9 | gem "stackprof" 10 | gem "pg" 11 | 12 | gem "benchmark-ips" 13 | gem "active_model_serializers", "~> 0.10" 14 | gem "terminal-table" 15 | gem "memory_profiler" 16 | end 17 | 18 | group :test do 19 | gem "faker" 20 | end 21 | 22 | group :development do 23 | gem "byebug" 24 | gem "rake" 25 | gem "rspec", "~> 3.0" 26 | gem "rake-compiler" 27 | end 28 | 29 | group :development, :test do 30 | gem "rubocop" 31 | 32 | gem "standard" 33 | gem "standard-performance" 34 | gem "rubocop-performance" 35 | gem "rubocop-rspec" 36 | end 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Yosi Attias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Panko 2 | 3 | ![Build Status](https://github.com/yosiat/panko_serializer/workflows/Panko%20Serializer%20CI/badge.svg?branch=master) 4 | 5 | Panko is a library which is inspired by ActiveModelSerializers 0.9 for serializing ActiveRecord/Ruby objects to JSON strings, fast. 6 | 7 | To achieve its [performance](https://panko.dev/performance/): 8 | 9 | * Oj - Panko relies on Oj since it's fast and allows for incremental serialization using `Oj::StringWriter` 10 | * Serialization Descriptor - Panko computes most of the metadata ahead of time, to save time later in serialization. 11 | * Type casting — Panko does type casting by itself, instead of relying on ActiveRecord. 12 | 13 | To dig deeper about the performance choices, read [Design Choices](https://panko.dev/design-choices/). 14 | 15 | 16 | Support 17 | ------- 18 | 19 | - [Documentation](https://panko.dev/) 20 | - [Getting Started](https://panko.dev/getting-started/) 21 | 22 | License 23 | ------- 24 | 25 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 26 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | require "json" 6 | require "terminal-table" 7 | require "rake/extensiontask" 8 | require "pty" 9 | require "rubocop/rake_task" 10 | 11 | gem = Gem::Specification.load(File.dirname(__FILE__) + "/panko_serializer.gemspec") 12 | 13 | Rake::ExtensionTask.new("panko_serializer", gem) do |ext| 14 | ext.lib_dir = "lib/panko" 15 | end 16 | 17 | Gem::PackageTask.new(gem) do |pkg| 18 | pkg.need_zip = pkg.need_tar = false 19 | end 20 | 21 | RSpec::Core::RakeTask.new(:spec) 22 | Rake::Task[:spec].prerequisites << :compile 23 | Rake::Task[:compile].prerequisites << :clean 24 | 25 | task default: :spec 26 | 27 | RuboCop::RakeTask.new 28 | 29 | def print_and_flush(str) 30 | print str 31 | $stdout.flush 32 | end 33 | 34 | def run_process(cmd) 35 | puts "> Running #{cmd}" 36 | lines = [] 37 | _stderr_reader, stderr_writer = IO.pipe 38 | PTY.spawn(cmd, err: stderr_writer.fileno) do |stdout, stdin, pid| 39 | stdout.each do |line| 40 | print_and_flush "." 41 | lines << line 42 | end 43 | rescue Errno::EIO 44 | # ignore this 45 | end 46 | 47 | lines 48 | rescue PTY::ChildExited 49 | puts "The child process exited! - #{cmd}" 50 | [] 51 | end 52 | 53 | def run_benchmarks(files, items_count: 2_300) 54 | headings = ["Benchmark", "ip/s", "allocs/retained"] 55 | files.each do |benchmark_file| 56 | lines = run_process "ITEMS_COUNT=#{items_count} RAILS_ENV=production ruby #{benchmark_file}" 57 | rows = lines.map do |line| 58 | row = JSON.parse(line) 59 | row.values 60 | rescue JSON::ParserError 61 | puts "> [ERROR] Failed running #{benchmark_file} - #{lines.join}" 62 | end 63 | 64 | puts "\n\n" 65 | title = File.basename(benchmark_file, ".rb") 66 | table = Terminal::Table.new title: title, headings: headings, rows: rows 67 | puts table 68 | end 69 | end 70 | 71 | namespace :benchmarks do 72 | desc "All" 73 | task :all do 74 | run_benchmarks Dir[File.join(__dir__, "benchmarks", "**", "bm_*")] 75 | end 76 | 77 | desc "Type Casts" 78 | task :type_casts do 79 | run_benchmarks Dir[File.join(__dir__, "benchmarks", "type_casts", "bm_*")], items_count: 0 80 | end 81 | 82 | desc "Sanity" 83 | task :sanity do 84 | puts Time.now.strftime("%d/%m %H:%M:%S") 85 | puts "==========================" 86 | 87 | run_benchmarks [ 88 | File.join(__dir__, "benchmarks", "sanity.rb") 89 | ], items_count: 2300 90 | 91 | puts "\n\n" 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /benchmarks/BENCHMARKS.md: -------------------------------------------------------------------------------- 1 | ## Initial state 2 | ``` 3 | Native_Posts_2300 57 ip/s 13802 allocs/op 4 | Native_Posts_50 2956 ip/s 302 allocs/op 5 | 6 | AMS_Simple_Posts_2300 21 ip/s 94309 allocs/op 7 | AMS_Simple_Posts_50 1008 ip/s 2059 allocs/op 8 | AMS_HasOne_Posts_2300 9 ip/s 147209 allocs/op 9 | AMS_HasOne_Posts_50 451 ip/s 3209 allocs/op 10 | 11 | Panko_HasOne_Posts_2300 164 ip/s 2372 allocs/op 12 | Panko_HasOne_Posts_50 6118 ip/s 122 allocs/op 13 | Panko_Reused_HasOne_Posts_2300 178 ip/s 2303 allocs/op 14 | Panko_Reused_HasOne_Posts_50 8203 ip/s 53 allocs/op 15 | 16 | Panko_Simple_Posts_2300 150 ip/s 2372 allocs/op 17 | Panko_Simple_Posts_50 5639 ip/s 122 allocs/op 18 | Panko_Reused_Simple_Posts_2300 180 ip/s 2303 allocs/op 19 | Panko_Reused_Simple_Posts_50 8388 ip/s 53 allocs/op 20 | ``` 21 | 22 | ## Refactorings, method call support, combining 23 | 24 | ### class eval 25 | ``` 26 | Panko_HasOne_Posts_2300 64 ip/s 9477 allocs/op 27 | Panko_HasOne_Posts_50 2397 ip/s 477 allocs/op 28 | Panko_Reused_HasOne_Posts_2300 70 ip/s 9423 allocs/op 29 | Panko_Reused_HasOne_Posts_50 2596 ip/s 423 allocs/op 30 | 31 | Panko_Simple_Posts_2300 191 ip/s 2472 allocs/op 32 | Panko_Simple_Posts_50 5128 ip/s 222 allocs/op 33 | Panko_Reused_Simple_Posts_2300 180 ip/s 2418 allocs/op 34 | Panko_Reused_Simple_Posts_50 5534 ip/s 168 allocs/op 35 | ``` 36 | 37 | ### instance eval 38 | ``` 39 | Panko_HasOne_Posts_2300 60 ip/s 9473 allocs/op 40 | Panko_HasOne_Posts_50 2399 ip/s 473 allocs/op 41 | Panko_Reused_HasOne_Posts_2300 66 ip/s 9419 allocs/op 42 | Panko_Reused_HasOne_Posts_50 2582 ip/s 419 allocs/op 43 | 44 | Panko_Simple_Posts_2300 195 ip/s 2470 allocs/op 45 | Panko_Simple_Posts_50 4838 ip/s 220 allocs/op 46 | Panko_Reused_Simple_Posts_2300 196 ip/s 2416 allocs/op 47 | Panko_Reused_Simple_Posts_50 6241 ip/s 166 allocs/op 48 | ``` 49 | -------------------------------------------------------------------------------- /benchmarks/allocs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "benchmarking_support" 4 | require_relative "app" 5 | require_relative "setup" 6 | 7 | require "memory_profiler" 8 | 9 | class PostFastSerializer < Panko::Serializer 10 | attributes :id, :body, :title, :author_id 11 | end 12 | 13 | def count_allocs(&) 14 | memory_report = MemoryProfiler.report(&) 15 | puts memory_report.pretty_print 16 | end 17 | 18 | posts = Post.all.to_a 19 | merged_options = {}.merge(each_serializer: PostFastSerializer) 20 | posts_array_serializer = Panko::ArraySerializer.new([], merged_options) 21 | 22 | # prints out 18402 23 | count_allocs { posts_array_serializer.serialize_to_json posts } 24 | -------------------------------------------------------------------------------- /benchmarks/app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | 5 | require "active_model" 6 | require "active_record" 7 | require "active_support" 8 | require "active_support/json" 9 | 10 | require "active_model_serializers" 11 | require "panko_serializer" 12 | -------------------------------------------------------------------------------- /benchmarks/benchmarking_support.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "benchmark/ips" 4 | require "json" 5 | require "memory_profiler" 6 | 7 | module Benchmark 8 | module Runner 9 | def data 10 | posts = Post.all.includes(:author).to_a 11 | posts_50 = posts.first(50).to_a 12 | posts_single = posts.first(1).to_a 13 | {all: posts, small: posts_50, single: posts_single} 14 | end 15 | 16 | def run(label = nil, time: 10, disable_gc: true, warmup: 3, &block) 17 | fail ArgumentError.new, "block should be passed" unless block 18 | 19 | GC.start 20 | 21 | if disable_gc 22 | GC.disable 23 | else 24 | GC.enable 25 | end 26 | 27 | memory_report = MemoryProfiler.report(&block) 28 | 29 | report = Benchmark.ips(time, warmup, true) do |x| 30 | x.report(label) { yield } 31 | end 32 | 33 | results = { 34 | label: label, 35 | ips: ActiveSupport::NumberHelper.number_to_delimited(report.entries.first.ips.round(2)), 36 | allocs: "#{memory_report.total_allocated}/#{memory_report.total_retained}" 37 | }.to_json 38 | 39 | puts results 40 | end 41 | end 42 | 43 | extend Benchmark::Runner 44 | end 45 | -------------------------------------------------------------------------------- /benchmarks/bm_ams_0_10.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "benchmarking_support" 4 | require_relative "app" 5 | require_relative "setup" 6 | 7 | # disable logging for benchmarks 8 | ActiveModelSerializers.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(File::NULL)) 9 | 10 | class AmsAuthorFastSerializer < ActiveModel::Serializer 11 | attributes :id, :name 12 | end 13 | 14 | class AmsPostFastSerializer < ActiveModel::Serializer 15 | attributes :id, :body, :title, :author_id, :created_at 16 | end 17 | 18 | class AmsPostWithHasOneFastSerializer < ActiveModel::Serializer 19 | attributes :id, :body, :title, :author_id 20 | 21 | has_one :author, serializer: AmsAuthorFastSerializer 22 | end 23 | 24 | def benchmark_ams(prefix, serializer, options = {}) 25 | merged_options = options.merge(each_serializer: serializer) 26 | 27 | posts = Benchmark.data[:all] 28 | 29 | Benchmark.run("AMS_#{prefix}_Posts_#{posts.count}") do 30 | ActiveModelSerializers::SerializableResource.new(posts, merged_options).to_json 31 | end 32 | 33 | posts_50 = Benchmark.data[:small] 34 | 35 | Benchmark.run("AMS_#{prefix}_Posts_50") do 36 | ActiveModelSerializers::SerializableResource.new(posts_50, merged_options).to_json 37 | end 38 | end 39 | 40 | benchmark_ams "Attributes_Simple", AmsPostFastSerializer 41 | benchmark_ams "Attributes_HasOne", AmsPostWithHasOneFastSerializer 42 | benchmark_ams "Attributes_Except", AmsPostWithHasOneFastSerializer, except: [:title] 43 | benchmark_ams "Attributes_Only", AmsPostWithHasOneFastSerializer, only: [:id, :body, :author_id, :author] 44 | -------------------------------------------------------------------------------- /benchmarks/bm_object_writer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "benchmarking_support" 4 | require_relative "app" 5 | 6 | Benchmark.run("ObjectWriter_OneProperty_PushValue") do 7 | writer = Panko::ObjectWriter.new 8 | 9 | writer.push_object 10 | writer.push_value "value1", "key1" 11 | writer.pop 12 | 13 | writer.output 14 | end 15 | 16 | Benchmark.run("ObjectWriter_TwoProperty_PushValue") do 17 | writer = Panko::ObjectWriter.new 18 | 19 | writer.push_object 20 | writer.push_value "value1", "key1" 21 | writer.push_value "value2", "key2" 22 | writer.pop 23 | 24 | writer.output 25 | end 26 | 27 | Benchmark.run("ObjectWriter_OneProperty_PushValuePushKey") do 28 | writer = Panko::ObjectWriter.new 29 | 30 | writer.push_object 31 | writer.push_key "key1" 32 | writer.push_value "value1" 33 | writer.pop 34 | 35 | writer.output 36 | end 37 | 38 | Benchmark.run("ObjectWriter_TwoProperty_PushValuePushKey") do 39 | writer = Panko::ObjectWriter.new 40 | 41 | writer.push_object 42 | writer.push_key "key1" 43 | writer.push_value "value1" 44 | 45 | writer.push_key "key2" 46 | writer.push_value "value2" 47 | writer.pop 48 | 49 | writer.output 50 | end 51 | 52 | Benchmark.run("ObjectWriter_NestedObject") do 53 | writer = Panko::ObjectWriter.new 54 | 55 | writer.push_object 56 | writer.push_value "value1", "key1" 57 | 58 | writer.push_object "key2" 59 | writer.push_value "value2", "key2" 60 | writer.pop 61 | 62 | writer.pop 63 | 64 | writer.output 65 | end 66 | -------------------------------------------------------------------------------- /benchmarks/bm_panko_json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "benchmarking_support" 4 | require_relative "app" 5 | require_relative "setup" 6 | 7 | class AuthorFastSerializer < Panko::Serializer 8 | attributes :id, :name 9 | end 10 | 11 | class PostFastSerializer < Panko::Serializer 12 | attributes :id, :body, :title, :author_id, :created_at 13 | end 14 | 15 | class PostFastWithJsonSerializer < Panko::Serializer 16 | attributes :id, :body, :title, :author_id, :created_at, :data 17 | end 18 | 19 | class PostFastWithMethodCallSerializer < Panko::Serializer 20 | attributes :id, :body, :title, :author_id, :method_call 21 | 22 | def method_call 23 | object.id * 2 24 | end 25 | end 26 | 27 | class PostWithHasOneFastSerializer < Panko::Serializer 28 | attributes :id, :body, :title, :author_id 29 | 30 | has_one :author, serializer: AuthorFastSerializer 31 | end 32 | 33 | class AuthorWithHasManyFastSerializer < Panko::Serializer 34 | attributes :id, :name 35 | 36 | has_many :posts, serializer: PostFastSerializer 37 | end 38 | 39 | def benchmark(prefix, serializer, options = {}) 40 | posts = Benchmark.data[:all] 41 | 42 | merged_options = options.merge(each_serializer: serializer) 43 | 44 | Benchmark.run("Panko_ActiveRecord_#{prefix}_Posts_#{posts.count}") do 45 | Panko::ArraySerializer.new(posts, merged_options).to_json 46 | end 47 | 48 | posts_50 = Benchmark.data[:small] 49 | 50 | Benchmark.run("Panko_ActiveRecord_#{prefix}_Posts_50") do 51 | Panko::ArraySerializer.new(posts_50, merged_options).to_json 52 | end 53 | end 54 | 55 | benchmark "Simple", PostFastSerializer 56 | benchmark "SimpleWithJson", PostFastWithJsonSerializer 57 | benchmark "HasOne", PostWithHasOneFastSerializer 58 | benchmark "SimpleWithMethodCall", PostFastWithMethodCallSerializer 59 | benchmark "Except", PostWithHasOneFastSerializer, except: [:title] 60 | benchmark "Only", PostWithHasOneFastSerializer, only: [:id, :body, :author_id, :author] 61 | -------------------------------------------------------------------------------- /benchmarks/bm_panko_object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "benchmarking_support" 4 | require_relative "app" 5 | require_relative "setup" 6 | 7 | class AuthorFastSerializer < Panko::Serializer 8 | attributes :id, :name 9 | end 10 | 11 | class PostFastSerializer < Panko::Serializer 12 | attributes :id, :body, :title, :author_id 13 | end 14 | 15 | class PostWithHasOneFastSerializer < Panko::Serializer 16 | attributes :id, :body, :title, :author_id 17 | 18 | has_one :author, serializer: AuthorFastSerializer 19 | end 20 | 21 | class AuthorWithHasManyFastSerializer < Panko::Serializer 22 | attributes :id, :name 23 | 24 | has_many :posts, serializer: PostFastSerializer 25 | end 26 | 27 | def benchmark(prefix, serializer, options = {}) 28 | posts = Benchmark.data[:all] 29 | 30 | merged_options = options.merge(each_serializer: serializer) 31 | 32 | Benchmark.run("Panko_ActiveRecord_#{prefix}_Posts_#{posts.count}") do 33 | Panko::ArraySerializer.new(posts, merged_options).to_a 34 | end 35 | 36 | posts_50 = Benchmark.data[:small] 37 | 38 | Benchmark.run("Panko_ActiveRecord_#{prefix}_Posts_50") do 39 | Panko::ArraySerializer.new(posts_50, merged_options).to_a 40 | end 41 | end 42 | 43 | benchmark "Simple", PostFastSerializer 44 | benchmark "HasOne", PostWithHasOneFastSerializer 45 | benchmark "Except", PostWithHasOneFastSerializer, except: [:title] 46 | benchmark "Only", PostWithHasOneFastSerializer, only: [:id, :body, :author_id, :author] 47 | -------------------------------------------------------------------------------- /benchmarks/bm_plain_object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "benchmarking_support" 4 | require_relative "app" 5 | 6 | class AuthorFastSerializer < Panko::Serializer 7 | attributes :id, :name 8 | end 9 | 10 | class PostFastSerializer < Panko::Serializer 11 | attributes :id, :body, :title, :author_id, :created_at 12 | end 13 | 14 | class PostFastWithMethodCallSerializer < Panko::Serializer 15 | attributes :id, :body, :title, :author_id, :method_call 16 | 17 | def method_call 18 | object.id * 2 19 | end 20 | end 21 | 22 | class PostWithHasOneFastSerializer < Panko::Serializer 23 | attributes :id, :body, :title, :author_id 24 | 25 | has_one :author, serializer: AuthorFastSerializer 26 | end 27 | 28 | class AuthorWithHasManyFastSerializer < Panko::Serializer 29 | attributes :id, :name 30 | 31 | has_many :posts, serializer: PostFastSerializer 32 | end 33 | 34 | class Post 35 | attr_accessor :id, :body, :title, :created_at, :author_id 36 | attr_reader :author 37 | 38 | def self.create(attrs) 39 | p = Post.new 40 | attrs.each do |k, v| 41 | p.send :"#{k}=", v 42 | end 43 | p 44 | end 45 | 46 | def author=(author) 47 | @author = author 48 | @author_id = author.id 49 | end 50 | end 51 | 52 | class Author 53 | attr_accessor :id, :name 54 | 55 | def self.create(attrs) 56 | a = Author.new 57 | attrs.each do |k, v| 58 | a.send :"#{k}=", v 59 | end 60 | a 61 | end 62 | end 63 | 64 | def benchmark_data 65 | posts = [] 66 | ENV.fetch("ITEMS_COUNT", "2300").to_i.times do |i| 67 | posts << Post.create( 68 | id: i, 69 | body: "something about how password restrictions are evil, and less secure, and with the math to prove it.", 70 | title: "Your bank is does not know how to do security", 71 | author: Author.create(id: i, name: "Preston Sego") 72 | ) 73 | end 74 | 75 | {all: posts, small: posts.first(50)} 76 | end 77 | 78 | def benchmark(prefix, serializer, options = {}) 79 | data = benchmark_data 80 | posts = data[:all] 81 | 82 | merged_options = options.merge(each_serializer: serializer) 83 | 84 | Benchmark.run("Panko_Plain_#{prefix}_Posts_#{posts.count}") do 85 | Panko::ArraySerializer.new(posts, merged_options).to_json 86 | end 87 | 88 | posts_50 = benchmark_data[:small] 89 | 90 | Benchmark.run("Panko_Plain_#{prefix}_Posts_50") do 91 | Panko::ArraySerializer.new(posts_50, merged_options).to_json 92 | end 93 | end 94 | 95 | benchmark "Simple", PostFastSerializer 96 | benchmark "HasOne", PostWithHasOneFastSerializer 97 | benchmark "SimpleWithMethodCall", PostFastWithMethodCallSerializer 98 | benchmark "Except", PostWithHasOneFastSerializer, except: [:title] 99 | benchmark "Only", PostWithHasOneFastSerializer, only: [:id, :body, :author_id, :author] 100 | -------------------------------------------------------------------------------- /benchmarks/bm_serialization_descriptor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "benchmarking_support" 4 | require "active_support" 5 | 6 | require "panko_serializer" 7 | 8 | def generate_attributes(count) 9 | (1..count).map { |i| :"attr_#{i}" } 10 | end 11 | 12 | class LeafASerializer < Panko::Serializer 13 | attributes(*generate_attributes(5)) 14 | end 15 | 16 | class LeafBSerializer < Panko::Serializer 17 | attributes(*generate_attributes(6)) 18 | end 19 | 20 | class ChildrenSerializer < Panko::Serializer 21 | attributes(*generate_attributes(28)) 22 | 23 | has_one :leaf_a, serializer: LeafASerializer 24 | has_one :leaf_b, serializer: LeafBSerializer 25 | 26 | def attr_1 27 | end 28 | 29 | def attr_2 30 | end 31 | 32 | def attr_3 33 | end 34 | 35 | def attr_4 36 | end 37 | 38 | def attr_5 39 | end 40 | end 41 | 42 | class ParentSerializer < Panko::Serializer 43 | attributes(*generate_attributes(46)) 44 | 45 | has_many :children, serializer: ChildrenSerializer 46 | 47 | def attr_1 48 | end 49 | 50 | def attr_2 51 | end 52 | 53 | def attr_3 54 | end 55 | 56 | def attr_4 57 | end 58 | end 59 | 60 | attrs = generate_attributes(21) 61 | attrs << :children 62 | filters = { 63 | instance: attrs, 64 | children: generate_attributes(11) 65 | } 66 | Benchmark.run("NoFilters") do 67 | Panko::SerializationDescriptor.build(ParentSerializer) 68 | end 69 | 70 | Benchmark.run("Attribute") do 71 | Panko::SerializationDescriptor.build(ParentSerializer, only: [:children]) 72 | end 73 | 74 | Benchmark.run("AssociationFilters") do 75 | Panko::SerializationDescriptor.build(ParentSerializer, only: filters) 76 | end 77 | -------------------------------------------------------------------------------- /benchmarks/bm_serializer_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "benchmarking_support" 4 | require_relative "app" 5 | 6 | class NotSerializer 7 | end 8 | 9 | class RealSerializer < Panko::Serializer 10 | end 11 | 12 | Benchmark.run("CantFindConst") do 13 | Panko::SerializerResolver.resolve("cant_find_const", Object) 14 | end 15 | 16 | Benchmark.run("NotSerializer") do 17 | Panko::SerializerResolver.resolve("not", Object) 18 | end 19 | 20 | Benchmark.run("RealSerializer") do 21 | Panko::SerializerResolver.resolve("real", Object) 22 | end 23 | -------------------------------------------------------------------------------- /benchmarks/bm_to_object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "benchmarking_support" 4 | require_relative "app" 5 | require_relative "setup" 6 | 7 | class PostWithAliasModel < ActiveRecord::Base 8 | self.table_name = "posts" 9 | 10 | alias_attribute :new_id, :id 11 | alias_attribute :new_body, :body 12 | alias_attribute :new_title, :title 13 | alias_attribute :new_author_id, :author_id 14 | alias_attribute :new_created_at, :created_at 15 | end 16 | 17 | class PostWithAliasFastSerializer < Panko::Serializer 18 | attributes :new_id, :new_body, :new_title, :new_author_id, :new_created_at 19 | end 20 | 21 | def benchmark_aliased(prefix, serializer, options = {}) 22 | posts = PostWithAliasModel.all.to_a 23 | posts_50 = posts.first(50).to_a 24 | 25 | merged_options = options.merge(each_serializer: serializer) 26 | 27 | Benchmark.run("Panko_#{prefix}_PostWithAliasModels_#{posts.count}") do 28 | Panko::ArraySerializer.new(posts, merged_options).to_json 29 | end 30 | 31 | Benchmark.run("Panko_#{prefix}_Posts_50") do 32 | Panko::ArraySerializer.new(posts_50, merged_options).to_json 33 | end 34 | end 35 | 36 | class AuthorFastSerializer < Panko::Serializer 37 | attributes :id, :name 38 | end 39 | 40 | class PostFastSerializer < Panko::Serializer 41 | attributes :id, :body, :title, :author_id, :created_at 42 | end 43 | 44 | class PostFastWithMethodCallSerializer < Panko::Serializer 45 | attributes :id, :body, :title, :author_id, :created_at, :method_call 46 | 47 | def method_call 48 | object.id 49 | end 50 | end 51 | 52 | class PostWithHasOneFastSerializer < Panko::Serializer 53 | attributes :id, :body, :title, :author_id, :created_at 54 | 55 | has_one :author, serializer: AuthorFastSerializer 56 | end 57 | 58 | class AuthorWithHasManyFastSerializer < Panko::Serializer 59 | attributes :id, :name 60 | 61 | has_many :posts, serializer: PostFastSerializer 62 | end 63 | 64 | def benchmark(prefix, serializer, options = {}) 65 | posts = Benchmark.data[:all] 66 | 67 | merged_options = options.merge(each_serializer: serializer) 68 | 69 | Benchmark.run("Panko_#{prefix}_Posts_#{posts.count}") do 70 | Panko::ArraySerializer.new(posts, merged_options).to_a 71 | end 72 | 73 | posts_50 = Benchmark.data[:small] 74 | 75 | Benchmark.run("Panko_#{prefix}_Posts_50") do 76 | Panko::ArraySerializer.new(posts_50, merged_options).to_a 77 | end 78 | end 79 | 80 | benchmark "Simple", PostFastSerializer 81 | benchmark "HasOne", PostWithHasOneFastSerializer 82 | -------------------------------------------------------------------------------- /benchmarks/profile.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "benchmarking_support" 4 | require_relative "app" 5 | require_relative "setup" 6 | 7 | require "memory_profiler" 8 | 9 | class NullLogger < Logger 10 | def initialize(*args) 11 | end 12 | 13 | def add(*args, &block) 14 | end 15 | end 16 | 17 | class BenchmarkApp < Rails::Application 18 | routes.append do 19 | get "/simple" => "main#simple" 20 | get "/text" => "main#text" 21 | 22 | get "/serialize_to_string" => "main#serialize_to_string" 23 | get "/serialize_to_stream" => "streaming#serialize_to_stream" 24 | end 25 | 26 | config.secret_token = "s" * 30 27 | config.secret_key_base = "foo" 28 | config.consider_all_requests_local = false 29 | 30 | # simulate production 31 | config.cache_classes = true 32 | config.eager_load = true 33 | config.action_controller.perform_caching = true 34 | 35 | # otherwise deadlock occured 36 | config.middleware.delete "Rack::Lock" 37 | 38 | # to disable log files 39 | config.logger = NullLogger.new 40 | config.active_support.deprecation = :log 41 | end 42 | 43 | BenchmarkApp.initialize! 44 | 45 | class AuthorFastSerializer < Panko::Serializer 46 | attributes :id, :name 47 | end 48 | 49 | class PostWithHasOneFastSerializer < Panko::Serializer 50 | attributes :id, :body, :title, :author_id 51 | 52 | has_one :author, serializer: AuthorFastSerializer 53 | end 54 | 55 | class StreamingController < ActionController::Base 56 | include ActionController::Live 57 | 58 | def serialize_to_stream 59 | headers["Content-Type"] = "application/json" 60 | 61 | data = Benchmark.data[:all] 62 | serializer = Panko::ArraySerializer.new([], each_serializer: PostWithHasOneFastSerializer) 63 | writer = Oj::StreamWriter.new(response.stream, mode: :rails) 64 | 65 | serializer.serialize_to_writer(data, writer) 66 | 67 | response.stream.close 68 | end 69 | end 70 | 71 | class RouteNotFoundError < StandardError; end 72 | 73 | def request(method, path) 74 | response = Rack::MockRequest.new(BenchmarkApp).send(method, path) 75 | if response.status.in?([404, 500]) 76 | raise RouteNotFoundError.new, "not found #{method.to_s.upcase} #{path}" 77 | end 78 | response 79 | end 80 | 81 | def memory(&) 82 | mem = MemoryProfiler.report(&) 83 | mem.pretty_print 84 | end 85 | 86 | memory { request(:get, "/serialize_to_stream") } 87 | -------------------------------------------------------------------------------- /benchmarks/sanity.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "benchmarking_support" 4 | require_relative "app" 5 | require_relative "setup" 6 | 7 | class PostWithAliasModel < ActiveRecord::Base 8 | self.table_name = "posts" 9 | 10 | alias_attribute :new_id, :id 11 | alias_attribute :new_body, :body 12 | alias_attribute :new_title, :title 13 | alias_attribute :new_author_id, :author_id 14 | alias_attribute :new_created_at, :created_at 15 | end 16 | 17 | class PostWithAliasFastSerializer < Panko::Serializer 18 | attributes :new_id, :new_body, :new_title, :new_author_id, :new_created_at 19 | end 20 | 21 | def benchmark_aliased(prefix, serializer, options = {}) 22 | posts = PostWithAliasModel.all.to_a 23 | posts_50 = posts.first(50).to_a 24 | 25 | merged_options = options.merge(each_serializer: serializer) 26 | 27 | Benchmark.run("Panko_#{prefix}_PostWithAliasModels_#{posts.count}") do 28 | Panko::ArraySerializer.new(posts, merged_options).to_json 29 | end 30 | 31 | Benchmark.run("Panko_#{prefix}_Posts_50") do 32 | Panko::ArraySerializer.new(posts_50, merged_options).to_json 33 | end 34 | end 35 | 36 | class AuthorFastSerializer < Panko::Serializer 37 | attributes :id, :name 38 | end 39 | 40 | class PostFastSerializer < Panko::Serializer 41 | attributes :id, :body, :title, :author_id, :created_at 42 | end 43 | 44 | class PostFastWithMethodCallSerializer < Panko::Serializer 45 | attributes :id, :body, :title, :author_id, :created_at, :method_call 46 | 47 | def method_call 48 | object.id 49 | end 50 | end 51 | 52 | class PostWithHasOneFastSerializer < Panko::Serializer 53 | attributes :id, :body, :title, :author_id, :created_at 54 | 55 | has_one :author, serializer: AuthorFastSerializer 56 | end 57 | 58 | class AuthorWithHasManyFastSerializer < Panko::Serializer 59 | attributes :id, :name 60 | 61 | has_many :posts, serializer: PostFastSerializer 62 | end 63 | 64 | def benchmark(prefix, serializer, options = {}) 65 | posts = Benchmark.data[:all] 66 | 67 | merged_options = options.merge(each_serializer: serializer) 68 | 69 | Benchmark.run("Panko_#{prefix}_Posts_#{posts.count}") do 70 | Panko::ArraySerializer.new(posts, merged_options).to_json 71 | end 72 | 73 | posts_50 = Benchmark.data[:small] 74 | 75 | Benchmark.run("Panko_#{prefix}_Posts_50") do 76 | Panko::ArraySerializer.new(posts_50, merged_options).to_json 77 | end 78 | end 79 | 80 | benchmark "Simple", PostFastSerializer 81 | benchmark "HasOne", PostWithHasOneFastSerializer 82 | benchmark "SimpleWithMethodCall", PostFastWithMethodCallSerializer 83 | benchmark_aliased "Simple (aliased)", PostWithAliasFastSerializer 84 | -------------------------------------------------------------------------------- /benchmarks/setup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ########################################### 4 | # Setup active record models 5 | ########################################## 6 | require "active_record" 7 | require "sqlite3" 8 | require "securerandom" 9 | 10 | # Change the following to reflect your database settings 11 | ActiveRecord::Base.establish_connection( 12 | adapter: "sqlite3", 13 | database: ":memory:" 14 | ) 15 | 16 | # Don't show migration output when constructing fake db 17 | ActiveRecord::Migration.verbose = false 18 | 19 | ActiveRecord::Schema.define do 20 | create_table :authors, force: true do |t| 21 | t.string :name 22 | t.timestamps(null: false) 23 | end 24 | 25 | create_table :posts, force: true do |t| 26 | t.text :body 27 | t.string :title 28 | t.references :author 29 | t.json :data 30 | t.timestamps(null: false) 31 | end 32 | end 33 | 34 | class Author < ActiveRecord::Base 35 | has_one :profile 36 | has_many :posts 37 | end 38 | 39 | class Post < ActiveRecord::Base 40 | belongs_to :author 41 | end 42 | 43 | Post.destroy_all 44 | Author.destroy_all 45 | 46 | # Build out the data to serialize 47 | Post.transaction do 48 | ENV.fetch("ITEMS_COUNT", "2300").to_i.times do 49 | Post.create( 50 | body: SecureRandom.hex(30), 51 | title: SecureRandom.hex(20), 52 | author: Author.create(name: SecureRandom.alphanumeric), 53 | data: {a: 1, b: 2, c: 3} 54 | ) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /benchmarks/type_casts/bm_active_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "support" 4 | 5 | def ar_type_convert(type_klass, from, to) 6 | converter = type_klass.new 7 | 8 | assert type_klass.name, converter.deserialize(from), to 9 | 10 | Benchmark.run("#{type_klass.name}_TypeCast") do 11 | converter.deserialize(from) 12 | end 13 | 14 | Benchmark.run("#{type_klass.name}_NoTypeCast") do 15 | converter.deserialize(to) 16 | end 17 | end 18 | 19 | def utc_ar_time 20 | date = DateTime.new(2017, 3, 4, 12, 45, 23) 21 | tz = ActiveSupport::TimeZone.new("UTC") 22 | from = date.in_time_zone(tz).iso8601 23 | 24 | type = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime.new 25 | converter = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type) 26 | 27 | Benchmark.run("#{tz}_#{type.class.name}_TypeCast") do 28 | converter.deserialize(from).iso8601 29 | end 30 | end 31 | 32 | def db_ar_time 33 | type = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime.new 34 | converter = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type) 35 | 36 | from = "2017-07-10 09:26:40.937392" 37 | 38 | Benchmark.run("ActiveRecord_Time_TypeCast_WithISO8601") do 39 | converter.deserialize(from).iso8601 40 | end 41 | end 42 | 43 | ar_type_convert ActiveRecord::Type::String, 1, "1" 44 | ar_type_convert ActiveRecord::Type::Text, 1, "1" 45 | ar_type_convert ActiveRecord::Type::Integer, "1", 1 46 | ar_type_convert ActiveRecord::Type::Float, "1.23", 1.23 47 | ar_type_convert ActiveRecord::Type::Boolean, "true", true 48 | ar_type_convert ActiveRecord::Type::Boolean, "t", true 49 | 50 | if check_if_exists "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json" 51 | ar_type_convert ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json, '{"a":1}', {a: 1} 52 | end 53 | if check_if_exists "ActiveRecord::Type::Json" 54 | ar_type_convert ActiveRecord::Type::Json, '{"a":1}', {a: 1} 55 | end 56 | 57 | db_ar_time 58 | utc_ar_time 59 | -------------------------------------------------------------------------------- /benchmarks/type_casts/bm_panko.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "support" 4 | 5 | def panko_type_convert(type_klass, from, to) 6 | converter = type_klass.new 7 | assert type_klass.name.to_s, Panko._type_cast(converter, from), to 8 | 9 | Benchmark.run("#{type_klass.name}_TypeCast") do 10 | Panko._type_cast(converter, from) 11 | end 12 | 13 | Benchmark.run("#{type_klass.name}_NoTypeCast") do 14 | Panko._type_cast(converter, to) 15 | end 16 | end 17 | 18 | def utc_panko_time 19 | date = DateTime.new(2017, 3, 4, 12, 45, 23) 20 | tz = ActiveSupport::TimeZone.new("UTC") 21 | from = date.in_time_zone(tz).iso8601 22 | 23 | type = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime.new 24 | converter = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type) 25 | 26 | to = Panko._type_cast(converter, from) 27 | 28 | Benchmark.run("#{tz}_#{type.class.name}_TypeCast") do 29 | Panko._type_cast(converter, from) 30 | end 31 | 32 | Benchmark.run("#{tz}_#{type.class.name}_NoTypeCast") do 33 | Panko._type_cast(converter, to) 34 | end 35 | end 36 | 37 | def db_panko_time 38 | type = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime.new 39 | converter = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type) 40 | 41 | from = "2017-07-10 09:26:40.937392" 42 | 43 | Benchmark.run("Panko_Time_TypeCast") do 44 | Panko._type_cast(converter, from) 45 | end 46 | end 47 | 48 | panko_type_convert ActiveRecord::Type::String, 1, "1" 49 | panko_type_convert ActiveRecord::Type::Text, 1, "1" 50 | panko_type_convert ActiveRecord::Type::Integer, "1", 1 51 | panko_type_convert ActiveRecord::Type::Float, "1.23", 1.23 52 | panko_type_convert ActiveRecord::Type::Boolean, "true", true 53 | panko_type_convert ActiveRecord::Type::Boolean, "t", true 54 | 55 | if check_if_exists "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json" 56 | panko_type_convert ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json, '{"a":1}', '{"a":1}' 57 | end 58 | if check_if_exists "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb" 59 | panko_type_convert ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb, '{"a":1}', '{"a":1}' 60 | end 61 | if check_if_exists "ActiveRecord::Type::Json" 62 | panko_type_convert ActiveRecord::Type::Json, '{"a":1}', '{"a":1}' 63 | end 64 | db_panko_time 65 | utc_panko_time 66 | -------------------------------------------------------------------------------- /benchmarks/type_casts/support.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record" 4 | require "active_record/connection_adapters/postgresql_adapter" 5 | require "active_model" 6 | require "active_support/all" 7 | require "pg" 8 | require "oj" 9 | 10 | require_relative "../benchmarking_support" 11 | require_relative "../../lib/panko_serializer" 12 | 13 | def assert(type_name, from, to) 14 | raise "#{type_name} - #{from.class} is not equals to #{to.class}" unless from.to_json == to.to_json 15 | end 16 | 17 | def check_if_exists(module_name) 18 | mod = begin 19 | module_name.constantize 20 | rescue 21 | nil 22 | end 23 | return true if mod 24 | false unless mod 25 | end 26 | 27 | Time.zone = "UTC" 28 | -------------------------------------------------------------------------------- /docs/docs/associations.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: associations 3 | title: Associations 4 | sidebar_label: Associations 5 | --- 6 | A serializer can define it's own associations - both `has_many` and `has_one` to serialize under the context of the object. 7 | 8 | For example: 9 | 10 | ```ruby 11 | 12 | class PostSerializer < Panko::Serializer 13 | attributes :title, :body 14 | 15 | has_one :author, serializer: AuthorSerializer 16 | has_many :comments, each_serializer: CommentSerializer 17 | end 18 | 19 | ``` 20 | 21 | ### Associations with aliases 22 | 23 | An association key name can be aliased with the `name` option. 24 | 25 | For example: 26 | the `actual_author` property will be converted to `alias_author`. 27 | 28 | ```ruby 29 | 30 | class PostSerializer < Panko::Serializer 31 | attributes :title, :body 32 | 33 | has_one :actual_author, serializer: AuthorSerializer, name: :alias_author 34 | has_many :comments, each_serializer: CommentSerializer 35 | end 36 | 37 | ``` 38 | 39 | ### Inference 40 | 41 | Panko can find the type of the serializer by looking at the relationship name, so instead of specifying 42 | the serializer at the above example, we can: 43 | 44 | ```ruby 45 | 46 | class PostSerializer < Panko::Serializer 47 | attributes :title, :body 48 | 49 | has_one :author 50 | has_many :comments 51 | end 52 | 53 | ``` 54 | 55 | The logic of inferencing is: 56 | 57 | - Take the name of the relationship (for example - `:author` / `:comments`) singularize and camelize it. 58 | - Look for const defined with the name above and "Serializer" suffix (by using `Object.const_get`). 59 | 60 | > If Panko can't find the serializer it will throw an error on startup time, for example: `Can't find serializer for PostSerializer.author has_one relationship`. 61 | 62 | ## Nested Filters 63 | 64 | As talked before, Panko allows you to filter the attributes of a serializer. 65 | But Panko lets you take that step further, and filters the attributes of you associations so you can re-use your serializers in your application. 66 | 67 | For example, let's say one portion of the application needs to serialize a list of posts but only with their - `title`, `body`, author's id and comments id. 68 | 69 | We can declare tailored serializer for this, or we can re-use the above defined serializer - `PostSerializer` and use nested filters. 70 | 71 | ```ruby 72 | 73 | posts = Post.all 74 | 75 | Panko::ArraySerializer.new(posts, each_serializer: PostSerializer, only: { 76 | instance: [:title, :body, :author, :comments], 77 | author: [:id], 78 | comments: [:id], 79 | }) 80 | 81 | ``` 82 | 83 | Let's dissect the `only` option we passed: 84 | 85 | - `instance` - list of attributes (and associations) we want to serialize for the current instance of the serializer, in this case - `PostSerializer`. 86 | - `author`, `comments` - here we specify the list of attributes we want to serialize for each association. 87 | 88 | It's important to note that Nested Filters are recursive, in other words, we can filter the association's associations. 89 | 90 | For example, `CommentSerializer` has an `has_one` association `Author`, and for each `comments.author` we can only serialize it's name. 91 | 92 | ```ruby 93 | 94 | posts = Post.all 95 | 96 | Panko::ArraySerializer.new(posts, only: { 97 | instance: [:title, :body, :author, :comments], 98 | author: [:id], 99 | comments: { 100 | instance: [:id, :author], 101 | author: [:name] 102 | } 103 | }) 104 | 105 | ``` 106 | 107 | As you see now in `comments` the `instance` have different meaning, the `CommentSerializer`. 108 | -------------------------------------------------------------------------------- /docs/docs/attributes.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: attributes 3 | title: Attributes 4 | sidebar_label: Attributes 5 | --- 6 | Attributes allow you to specify which record attributes you want to serialize. 7 | 8 | There are two types of attributes: 9 | 10 | - Field - simple columns defined on the record it self. 11 | - Virtual/Method - this allows to include properties beyond simple fields. 12 | 13 | ```ruby 14 | 15 | class UserSerializer < Panko::Serializer 16 | attributes :full_name 17 | 18 | def full_name 19 | "#{object.first_name} #{object.last_name}" 20 | end 21 | end 22 | 23 | ``` 24 | 25 | ## Field Attributes 26 | 27 | Using field attributes you can control which columns of the given ActiveRecord object you want to serialize. 28 | 29 | Instead of relying on ActiveRecord to do it's type casting, Panko does on it's own for performance reasons (read more in [Design Choices](design-choices.md#type-casting)). 30 | 31 | ## Method Attributes 32 | 33 | Method attributes are used when your serialized values can be derived from the object you are serializing. 34 | 35 | The serializer's attribute methods can access the object being serialized as `object`: 36 | 37 | ```ruby 38 | 39 | class PostSerializer < Panko::Serializer 40 | attributes :author_name 41 | 42 | def author_name 43 | "#{object.author.first_name} #{object.author.last_name}" 44 | end 45 | end 46 | 47 | ``` 48 | 49 | Another useful thing you can pass your serializer is `context`, a `context` is a bag of data whom your serializer may need. 50 | 51 | For example, here we will pass feature flags: 52 | 53 | ```ruby 54 | 55 | class UserSerializer < Panko::Serializer 56 | attributes :id, :email 57 | 58 | def feature_flags 59 | context[:feature_flags] 60 | end 61 | end 62 | 63 | serializer = UserSerializer.new(context: { 64 | feature_flags: FeatureFlags.all 65 | }) 66 | 67 | serializer.serialize(User.first) 68 | 69 | ``` 70 | 71 | ## Filters 72 | 73 | Filters allows us to reduce the amount of attributes we can serialize, therefore reduce the data usage & performance of serializing. 74 | 75 | There are two types of filters: 76 | 77 | - only - use those attributes **only** and nothing else. 78 | - except - all attributes **except** those attributes. 79 | 80 | Usage example: 81 | 82 | ```ruby 83 | 84 | class UserSerializer < Panko::Serializer 85 | attributes :id, :name, :email 86 | end 87 | 88 | # this line will return { 'name': '..' } 89 | UserSerializer.new(only: [:name]).serialize(User.first) 90 | 91 | # this line will return { 'id': '..', 'email': ... } 92 | UserSerializer.new(except: [:name]).serialize(User.first) 93 | 94 | ``` 95 | 96 | > **Note** that if you want to user filter on an associations, the `:name` property is not taken into account. 97 | If you have a `has_many :state_transitions, name: :history` association defined, the key to use in filters is 98 | `:state_transitions` (e.g. `{ except: [:state_transitions] }`). 99 | 100 | ## Filters For 101 | 102 | Sometimes you find yourself having the same filtering logic in actions. In order to 103 | solve this duplication, Panko allows you to write the filters in the serializer. 104 | 105 | ```ruby 106 | 107 | class UserSerializer < Panko::Serializer 108 | attributes :id, :name, :email 109 | 110 | def self.filters_for(context, scope) 111 | { 112 | only: [:name] 113 | } 114 | end 115 | end 116 | 117 | # this line will return { 'name': '..' } 118 | UserSerializer.serialize(User.first) 119 | 120 | ``` 121 | 122 | > See discussion in: [https:](https://github.com/yosiat/panko_serializer/issues/16) 123 | 124 | ## Aliases 125 | 126 | Let's say we have an attribute name that we want to expose to client as different name, the current way of doing so is using method attribute, for example: 127 | 128 | ```ruby 129 | 130 | class PostSerializer < Panko::Serializer 131 | attributes :published_at 132 | 133 | def published_at 134 | object.created_at 135 | end 136 | end 137 | 138 | ``` 139 | 140 | The downside of this approach is that `created_at` skips Panko's type casting, therefore we get a direct hit on performance. 141 | 142 | To fix this, we can use aliases: 143 | 144 | ```ruby 145 | 146 | class PostSerializer < Panko::Serializer 147 | aliases created_at: :published_at 148 | end 149 | 150 | ``` 151 | -------------------------------------------------------------------------------- /docs/docs/design-choices.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: design-choices 3 | title: Design Choices 4 | sidebar_label: Design Choices 5 | --- 6 | In short, Panko is a serializer for ActiveRecord objects (it can't serialize any other object), which strives for high performance & simple API (which is inspired by ActiveModelSerializers). 7 | 8 | Its performance is achieved by: 9 | 10 | - `Oj::StringWriter` - I will elaborate later. 11 | - Type casting — instead of relying on ActiveRecord to do its type cast, Panko is doing it by itself. 12 | - Figuring out the metadata, ahead of time — therefore, we ask less questions during the `serialization loop`. 13 | 14 | ## Serialization overview 15 | 16 | First, let's start with an overview. Let's say we want to serialize an `User` object, which has 17 | `first_name`, `last_name`, `age`, and `email` properties. 18 | 19 | The serializer definition will be something like this: 20 | 21 | ```ruby 22 | 23 | class UserSerializer < Panko::Serializer 24 | attributes :name, :age, :email 25 | 26 | def name 27 | "#{object.first_name} #{object.last_name}" 28 | end 29 | end 30 | 31 | ``` 32 | 33 | And the usage of this serializer will be: 34 | 35 | ```ruby 36 | 37 | # fetch user from database 38 | user = User.first 39 | 40 | # create serializer, with empty options 41 | serializer = UserSerializer.new 42 | 43 | # serialize to JSON 44 | serializer.serialize_to_json(user) 45 | 46 | ``` 47 | 48 | Let's go over the steps that Panko will execute behind the scenes for this flow. 49 | _I will skip the serializer definition part, because it's fairly simple and straightforward (see `lib/panko/serializer.rb`)._ 50 | 51 | First step, while initializing the UserSerializer, we will create a **Serialization Descriptor** for this class. 52 | Serialization Descriptor's goal is to answer those questions: 53 | 54 | - Which fields do we have? In our case, `:age`, `:email`. 55 | - Which method fields do we have? In our case `:name`. 56 | - Which associations do we have (and their serialization descriptors)? 57 | 58 | The serialization description is also responsible for filtering the attributes (`only` \\ `except`). 59 | 60 | Now, that we have the serialization descriptor, we are finished with the Ruby part of Panko, and all we did here is done in _initialization time_ and now we move to C code. 61 | 62 | In C land, we take the `user` object and the serialization descriptor, and start the serialization process which is separated to 4 parts: 63 | 64 | - Serializing Fields - looping through serialization descriptor's `fields` and read them from the ActiveRecord object (see `Type Casting`) and write them to the writer. 65 | - Serializing Method Fields - creating (a cached) serializer instance, setting its `@object` and `@context`, calling all the method fields and writing them to the writer. 66 | - Serializing associations — this is simple, once we have fields + method fields, we just repeat the process. 67 | 68 | Once this is finished, we have a nice JSON string. 69 | Now let's dig deeper. 70 | 71 | ## Interesting parts 72 | 73 | ### Oj::StringWriter 74 | 75 | If you read the code of ActiveRecord serialization code in Ruby, you will observe this flow: 76 | 77 | 1. Get an array of ActiveRecord objects (`User.all` for example). 78 | 2. Build a new array of hashes where each hash is an `User` with the attributes we selected. 79 | 3. The JSON serializer, takes this array of hashes and loop them, and converts it to a JSON string. 80 | 81 | This entire process is expensive in terms of Memory & CPU, and this where the combination of Panko and Oj::StringWriter really shines. 82 | 83 | In Panko, the serialization process of the above is: 84 | 85 | 1. Get an array of ActiveRecord objects (`User.all` for example). 86 | 2. Create `Oj::StringWriter` and feed the values to it, via `push_value` / `push_object` / `push_object` and behind the scene, `Oj::StringWriter` will serialize the objects incrementally into a string. 87 | 3. Get from `Oj::StringWriter` the completed JSON string — which is a no-op, since `Oj::StringWriter` already built the string. 88 | 89 | ### Figuring out the metadata, ahead of time. 90 | 91 | Another observation I noticed in the Ruby serializers is that they ask and do a lot in a serialization loop: 92 | 93 | - Is this field a method? is it a property? 94 | - Which fields and associations do I need for the serializer to consider the `only` and `except` options? 95 | - What is the serializer of this has_one association? 96 | 97 | Panko tries to ask the bare minimum in serialization by building `Serialization Descriptor` for each serialization and caching it. 98 | 99 | The Serialization Descriptor will do the filtering of `only` and `except` and will check if a field is a method or not (therefore Panko doesn't have list of `attributes`). 100 | 101 | ### Type Casting 102 | 103 | This is the final part, which helped yield most of the performance improvements. 104 | In ActiveRecord, when we read the value of an attribute, it does type casting of the DB value to its real Ruby type. 105 | 106 | For example, time strings are converted to Time objects, Strings are duplicated, and Integers are converted from their values to Number. 107 | 108 | This type casting is really expensive, as it's responsible for most of the allocations in the serialization flow and most of them can be "relaxed". 109 | 110 | If we think about it, we don't need to duplicate strings or convert time strings to time objects or even parse JSON strings for the JSON serialization process. 111 | 112 | What Panko does is that if we have ActiveRecord type string, we won't duplicate it. 113 | If we have an integer string value, we will convert it to an integer, and the same goes for other types. 114 | 115 | All of these conversions are done in C, which of course yields a big performance improvement. 116 | 117 | #### Time type casting 118 | 119 | While you read Panko source code, you will encounter the time type casting and immediately you will have a "WTF?" moment. 120 | 121 | The idea behind the time type casting code relies on the end result of JSON type casting — what we need in order to serialize Time to JSON? UTC ISO8601 time format representation. 122 | 123 | The time type casting works as follows: 124 | 125 | - If it's a string that ends with `Z`, and the strings matches the UTC ISO8601 regex, then we just return the string. 126 | - If it's a string and it doesn't follow the rules above, we check if it's a timestamp in database format and convert it via regex + string concat to UTC ISO8601 - Yes, there is huge assumption here, that the database returns UTC timestamps — this will be configurable (before Panko official release). 127 | - If it's none of the above, I will let ActiveRecord type casting do it's magic. 128 | -------------------------------------------------------------------------------- /docs/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: getting-started 3 | title: Getting Started 4 | sidebar_label: Getting Started 5 | --- 6 | ## Installation 7 | 8 | To install Panko, all you need is to add it to your Gemfile: 9 | 10 | ```ruby 11 | 12 | gem "panko_serializer" 13 | 14 | ``` 15 | 16 | Then, install it on the command line: 17 | 18 | ``` 19 | 20 | bundle install 21 | 22 | ``` 23 | 24 | ## Creating your first serializer 25 | 26 | Let's create a serializer and use it inside of a Rails controller: 27 | 28 | ```ruby 29 | class PostSerializer < Panko::Serializer 30 | attributes :title 31 | end 32 | 33 | class UserSerializer < Panko::Serializer 34 | attributes :id, :name, :age 35 | 36 | has_many :posts, serializer: PostSerializer 37 | end 38 | ``` 39 | 40 | ### Serializing an object 41 | 42 | And now serialize a single object: 43 | 44 | ```ruby 45 | 46 | # Using Oj serializer 47 | PostSerializer.new.serialize_to_json(Post.first) 48 | 49 | # or, similar to #serializable_hash 50 | PostSerializer.new.serialize(Post.first).to_json 51 | 52 | ``` 53 | 54 | ### Using the serializers in a controller 55 | 56 | As you can see, defining serializers is simple and resembles ActiveModelSerializers 0.9. 57 | To utilize the `UserSerializer` inside a Rails controller and serialize some users, all we need to do is: 58 | 59 | ```ruby 60 | 61 | class UsersController < ApplicationController 62 | def index 63 | users = User.includes(:posts).all 64 | render json: Panko::ArraySerializer.new(users, each_serializer: UserSerializer).to_json 65 | end 66 | end 67 | 68 | ``` 69 | 70 | And voila, we have an endpoint which serializes users using Panko! 71 | -------------------------------------------------------------------------------- /docs/docs/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: index 3 | title: Introduction 4 | sidebar_label: Introduction 5 | slug: / 6 | --- 7 | Panko is a library which is inspired by ActiveModelSerializers 0.9 for serializing ActiveRecord/Ruby objects to JSON strings, fast. 8 | 9 | To achieve it's [performance](https://panko.dev/docs/performance/): 10 | 11 | - Oj - Panko relies on Oj since it's fast and allow to serialize incrementally using `Oj::StringWriter`. 12 | - Serialization Descriptor - Panko computes most of the metadata ahead of time, to save time later in serialization. 13 | - Type casting — Panko does type casting by itself, instead of relying on ActiveRecord. 14 | -------------------------------------------------------------------------------- /docs/docs/performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: performance 3 | title: Performance 4 | sidebar_label: Performance 5 | --- 6 | The performance of Panko is measured using microbenchmarks and load testing. 7 | 8 | ## Microbenchmarks 9 | 10 | The following microbenchmarks are run on MacBook Pro (16-inch, 2021, M1 Max), Ruby 3.2.0 with Rails 7.0.5 11 | demonstrating the performance of ActiveModelSerializers 0.10.13 and Panko 0.8.0. 12 | 13 | | Benchmark | AMS ip/s | Panko ip/s | 14 | | ----------------- | -------- | ---------- | 15 | | Simple_Posts_2300 | 11.72 | 523.05 | 16 | | Simple_Posts_50 | 557.29 | 23,011.9 | 17 | | HasOne_Posts_2300 | 5.91 | 233.44 | 18 | | HasOne_Posts_50 | 285.8 | 10,362.79 | 19 | 20 | ## Real-world benchmark 21 | 22 | The real-world benchmark here is an endpoint which serializes 7,884 entries with 48 attributes and no associations. 23 | The benchmark took place in an environment that simulates production environment and run using `wrk` from machine on the same cluster. 24 | 25 | | Metric | AMS | Panko | 26 | | ------------------ | ----- | ----- | 27 | | Avg Response Time | 4.89s | 1.48s | 28 | | Max Response Time | 5.42s | 1.83s | 29 | | 99th Response Time | 5.42s | 1.74s | 30 | | Total Requests | 61 | 202 | 31 | 32 | _Thanks to [Bringg](https://www.bringg.com) for providing the infrastructure for the benchmarks._ 33 | -------------------------------------------------------------------------------- /docs/docs/response-bag.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: response-bag 3 | title: Response 4 | sidebar_label: Response 5 | --- 6 | Let's say you have some JSON payload which is constructed using Panko serialization result, 7 | like this: 8 | 9 | ```ruby 10 | 11 | class PostsController < ApplicationController 12 | def index 13 | posts = Post.all 14 | render json: { 15 | success: true, 16 | total_count: posts.count, 17 | posts: Panko::ArraySerializer.new(posts, each_serializer: PostSerializer).to_json 18 | } 19 | end 20 | end 21 | 22 | ``` 23 | 24 | The output of the above will be a JSON string (for `posts`) inside a JSON string and this were `Panko::Response` shines. 25 | 26 | ```ruby 27 | 28 | class PostsController < ApplicationController 29 | def index 30 | posts = Post.all 31 | render json: Panko::Response.new( 32 | success: true, 33 | total_count: posts.count, 34 | posts: Panko::ArraySerializer.new(posts, each_serializer: PostSerializer) 35 | ) 36 | end 37 | end 38 | 39 | ``` 40 | 41 | And everything will work as expected! 42 | 43 | For a single object serialization, we need to use a different API (since `Panko::Serializer` doesn't accept an object in it's constructor): 44 | 45 | ```ruby 46 | 47 | class PostsController < ApplicationController 48 | def show 49 | post = Post.find(params[:id]) 50 | 51 | render( 52 | json: Panko::Response.create do |r| 53 | { 54 | success: true, 55 | post: r.serializer(post, PostSerializer) 56 | } 57 | end 58 | ) 59 | end 60 | end 61 | 62 | ``` 63 | 64 | ## JsonValue 65 | 66 | Let's take the above example further, we will serialize the posts and cache it as JSON string in our Cache. 67 | Now, you can wrap the cached value with `Panko::JsonValue`, like here: 68 | 69 | ```ruby 70 | 71 | class PostsController < ApplicationController 72 | def index 73 | posts = Cache.get("/posts") 74 | 75 | render json: Panko::Response.new( 76 | success: true, 77 | total_count: posts.count, 78 | posts: Panko::JsonValue.from(posts) 79 | ) 80 | end 81 | end 82 | 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "title": "Panko Serializers", 3 | "tagline": "High Performance JSON Serialization for ActiveRecord & Ruby Objects", 4 | "url": "https://panko.dev", 5 | "baseUrl": "/", 6 | "organizationName": "yosiat", 7 | "projectName": "panko_serializer", 8 | "favicon": "favicon.ico", 9 | "customFields": { 10 | "repoPath": "yosiat/panko_serializer", 11 | "repoUrl": "https://github.com/yosiat/panko_serializer", 12 | }, 13 | "onBrokenLinks": "log", 14 | "onBrokenMarkdownLinks": "log", 15 | "presets": [ 16 | [ 17 | "@docusaurus/preset-classic", 18 | { 19 | "docs": { 20 | "path": "./docs", 21 | "showLastUpdateAuthor": false, 22 | "showLastUpdateTime": false, 23 | "sidebarPath": "./sidebars.json", 24 | "routeBasePath": process.env.NODE_ENV === 'production' ? '/docs' : '/', 25 | }, 26 | "blog": false, 27 | "pages": false, 28 | "theme": { 29 | "customCss": "./src/css/customTheme.css" 30 | } 31 | } 32 | ] 33 | ], 34 | "plugins": [], 35 | "themeConfig": { 36 | colorMode: { 37 | defaultMode: 'light', 38 | disableSwitch: true, 39 | }, 40 | "navbar": { 41 | "title": "Panko Serializers", 42 | "items": [ 43 | { 44 | "to": "introduction", 45 | "label": "Docs", 46 | "position": "left" 47 | } 48 | ] 49 | }, 50 | "image": "img/undraw_online.svg", 51 | "footer": { 52 | "links": [ 53 | { 54 | title: "GitHub", 55 | items: [ 56 | { 57 | label: "Repository", 58 | href: "https://github.com/yosiat/panko_serializer" 59 | }, 60 | { 61 | label: "Discussions", 62 | href: "https://github.com/yosiat/panko_serializer/discussions" 63 | }, 64 | { 65 | label: "Issues", 66 | href: "https://github.com/yosiat/panko_serializer/issues" 67 | }, 68 | { 69 | "html": ` 70 |