Showing preview only (226K chars total). Download the full file or copy to clipboard to get everything.
Repository: yosiat/panko_serializer
Branch: master
Commit: 552ee5c99e29
Files: 91
Total size: 204.4 KB
Directory structure:
gitextract_w1z8tyjv/
├── .clang-format
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── database_matrix.yml
│ ├── docs.yml
│ ├── lint.yml
│ └── tests.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Appraisals
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── benchmarks/
│ ├── object_writer.rb
│ ├── panko_json.rb
│ ├── panko_object.rb
│ ├── plain_object.rb
│ ├── support/
│ │ ├── benchmark.rb
│ │ ├── datasets.rb
│ │ └── setup.rb
│ └── type_casts/
│ ├── generic.rb
│ ├── mysql.rb
│ ├── postgresql.rb
│ └── sqlite.rb
├── docs/
│ ├── CNAME
│ ├── Gemfile
│ ├── _config.yml
│ ├── associations.md
│ ├── attributes.md
│ ├── design-choices.md
│ ├── getting-started.md
│ ├── index.md
│ ├── performance.md
│ ├── reference.md
│ └── response-bag.md
├── 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.2.0.gemfile
│ ├── 8.0.0.gemfile
│ └── 8.1.0.gemfile
├── 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/
├── features/
│ ├── active_record_serialization_spec.rb
│ ├── array_serializer_spec.rb
│ ├── associations_spec.rb
│ ├── attributes_spec.rb
│ ├── context_and_scope_spec.rb
│ ├── filtering_spec.rb
│ ├── hash_serialization_spec.rb
│ └── poro_serialization_spec.rb
├── spec_helper.rb
├── support/
│ └── database_config.rb
└── unit/
├── panko/
│ ├── array_serializer_spec.rb
│ ├── object_writer_spec.rb
│ ├── response_spec.rb
│ ├── serialization_descriptor_spec.rb
│ └── serializer_spec.rb
├── serializer_resolver_spec.rb
└── type_cast_spec.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .clang-format
================================================
---
Language: Cpp
BasedOnStyle: Google
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "bundler"
directory: "/docs"
schedule:
interval: "weekly"
groups:
docs-dependencies:
patterns:
- "*"
- package-ecosystem: "bundler"
directory: "/gemfiles"
schedule:
interval: "weekly"
groups:
gemfiles-dependencies:
patterns:
- "*"
================================================
FILE: .github/workflows/database_matrix.yml
================================================
name: Database Tests
on: [push, pull_request]
jobs:
database-matrix:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby: ["3.4", "4.0"]
rails: ["7.2.0", "8.0.0"]
database: ["sqlite", "postgresql", "mysql"]
services:
postgres:
image: postgres:17
env:
POSTGRES_DB: panko_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
mysql:
image: mysql:8.0
env:
MYSQL_DATABASE: panko_test
MYSQL_ROOT_PASSWORD: password
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
ports:
- 3306:3306
steps:
- name: Install system dependencies
run: |
sudo apt update -y
sudo apt install -y libsqlite3-dev libpq-dev libmysqlclient-dev
- uses: actions/checkout@v6
- name: Configure MySQL authentication
if: matrix.database == 'mysql'
run: |
# Configure MySQL to use mysql_native_password for Trilogy compatibility
mysql -h 127.0.0.1 -P 3306 -u root -ppassword -e "
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'password';
FLUSH PRIVILEGES;
"
- name: Set up Ruby ${{ matrix.ruby }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
working-directory: .
env:
BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
DB: ${{ matrix.database }}
- name: Compile & test
env:
BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
DB: ${{ matrix.database }}
POSTGRES_HOST: localhost
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_PORT: 5432
MYSQL_HOST: localhost
MYSQL_USER: root
MYSQL_PASSWORD: password
MYSQL_PORT: 3306
run: |
bundle exec rake
================================================
FILE: .github/workflows/docs.yml
================================================
name: Docs Publishing
on:
push:
branches: [master]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "4.0"
bundler-cache: true
working-directory: docs
- name: Setup Pages
id: pages
uses: actions/configure-pages@v6
- name: Build with Jekyll
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
env:
JEKYLL_ENV: production
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: docs/_site
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install deps
run: |
sudo apt update -y
sudo apt install -y libsqlite3-dev
- name: Lint Ruby code
uses: ruby/setup-ruby@v1
with:
ruby-version: 3
bundler-cache: true
- run: |
bundle exec rake rubocop
- name: Lint C
uses: jidicula/clang-format-action@v4.16.0
with:
clang-format-version: "16"
check-path: "ext/panko_serializer"
fallback-style: "Google"
================================================
FILE: .github/workflows/tests.yml
================================================
name: Panko Serializer CI
on: [push, pull_request]
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby: ["3.2", "3.3", "3.4", "4.0"]
rails: ["7.2.0", "8.0.0", "8.1.0"]
steps:
- name: Install deps
run: |
sudo apt update -y
sudo apt install -y libsqlite3-dev
- uses: actions/checkout@v6
- name: Set up Ruby ${{ matrix.ruby }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
working-directory: .
env:
BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
- name: Compile & test
env:
BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
run: |
bundle exec rake
================================================
FILE: .gitignore
================================================
/.bundle/
/.yardoc
/Gemfile.lock
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
/vendor/bundle/
.byebug_history
*.bundle
.DS_Store
# rspec failure tracking
.rspec_status
node_modules
/docs/_site/
/docs/.jekyll-cache/
================================================
FILE: .rspec
================================================
--format documentation
--color
================================================
FILE: .rubocop.yml
================================================
# We want Exclude directives from different
# config files to get merged, not overwritten
inherit_mode:
merge:
- Exclude
require:
- standard
plugins:
- rubocop-performance
- standard-performance
- rubocop-rspec
inherit_gem:
standard: config/base.yml
standard-performance: config/base.yml
AllCops:
TargetRubyVersion: 3.1
SuggestExtensions: false
NewCops: disable
Exclude:
- ext/**/*
- gemfiles/**/*
Style/FrozenStringLiteralComment:
Enabled: true
EnforcedStyle: always
SafeAutoCorrect: true
# TODO: need to work on specs.
RSpec:
Enabled: false
Lint/ConstantDefinitionInBlock:
Exclude:
- spec/**/*
================================================
FILE: Appraisals
================================================
# frozen_string_literal: true
appraise "7.2.0" do
gem "activesupport", "~> 7.2.0"
gem "activemodel", "~> 7.2.0"
gem "activerecord", "~> 7.2.0", group: :test
gem "trilogy"
gem "sqlite3", "~> 1.4"
end
appraise "8.0.0" do
gem "activesupport", "~> 8.0.0"
gem "activemodel", "~> 8.0.0"
gem "activerecord", "~> 8.0.0", group: :test
gem "trilogy"
gem "sqlite3", ">= 2.1"
end
appraise "8.1.0" do
gem "activesupport", "~> 8.1.0"
gem "activemodel", "~> 8.1.0"
gem "activerecord", "~> 8.1.0", group: :test
gem "trilogy"
gem "sqlite3", ">= 2.1"
end
================================================
FILE: Gemfile
================================================
# frozen_string_literal: true
source "https://rubygems.org"
gemspec
group :benchmarks do
gem "vernier"
gem "stackprof"
gem "pg"
gem "benchmark-ips"
gem "memory_profiler"
end
group :test do
gem "faker"
gem "temping"
end
group :development do
gem "byebug"
gem "rake"
gem "rspec", "~> 3.0"
gem "rake-compiler"
end
group :development, :test do
gem "rubocop"
gem "standard"
gem "standard-performance"
gem "rubocop-performance"
gem "rubocop-rspec"
end
================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)
Copyright (c) 2017 Yosi Attias
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Panko

Panko is a library which is inspired by ActiveModelSerializers 0.9 for serializing ActiveRecord/Ruby objects to JSON strings, fast.
To achieve its [performance](https://panko.dev/performance):
* Oj - Panko relies on Oj since it's fast and allows for incremental serialization using `Oj::StringWriter`
* Serialization Descriptor - Panko computes most of the metadata ahead of time, to save time later in serialization.
* Type casting — Panko does type casting by itself, instead of relying on ActiveRecord.
To dig deeper about the performance choices, read [Design Choices](https://panko.dev/design-choices).
Support
-------
- [Documentation](https://panko.dev/)
- [Getting Started](https://panko.dev/getting-started)
License
-------
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
================================================
FILE: Rakefile
================================================
# frozen_string_literal: true
require "bundler/gem_tasks"
require "rspec/core/rake_task"
require "rubocop/rake_task"
require "rake/extensiontask"
gem = Gem::Specification.load(File.dirname(__FILE__) + "/panko_serializer.gemspec")
Rake::ExtensionTask.new("panko_serializer", gem) do |ext|
ext.lib_dir = "lib/panko"
end
Gem::PackageTask.new(gem) do |pkg|
pkg.need_zip = pkg.need_tar = false
end
RSpec::Core::RakeTask.new(:spec)
Rake::Task[:spec].prerequisites << :compile
Rake::Task[:compile].prerequisites << :clean
task default: :spec
RuboCop::RakeTask.new
namespace :benchmarks do
desc "Run all benchmarks"
task :all do
files = Dir["benchmarks/*.rb", "benchmarks/type_casts/*.rb"].sort
files.each { |f| system("bundle", "exec", "ruby", f) || abort("FAILED: #{f}") }
end
desc "Run benchmarks matching NAME (e.g., rake benchmarks:run[type_casts:postgresql])"
task :run, [:name] do |_, args|
path = args[:name].tr(":", "/")
files = Dir["benchmarks/#{path}.rb", "benchmarks/#{path}/*.rb"].sort
abort "No benchmark files matching '#{args[:name]}'" if files.empty?
files.each { |f| system("bundle", "exec", "ruby", f) || abort("FAILED: #{f}") }
end
end
================================================
FILE: benchmarks/object_writer.rb
================================================
# frozen_string_literal: true
require_relative "support/benchmark"
require "panko_serializer"
benchmark("1 property, push_value") do
writer = Panko::ObjectWriter.new
writer.push_object
writer.push_value "value1", "key1"
writer.pop
writer.output
end
benchmark("2 properties, push_value") do
writer = Panko::ObjectWriter.new
writer.push_object
writer.push_value "value1", "key1"
writer.push_value "value2", "key2"
writer.pop
writer.output
end
benchmark("1 property, push_key+push_value") do
writer = Panko::ObjectWriter.new
writer.push_object
writer.push_key "key1"
writer.push_value "value1"
writer.pop
writer.output
end
benchmark("2 properties, push_key+push_value") do
writer = Panko::ObjectWriter.new
writer.push_object
writer.push_key "key1"
writer.push_value "value1"
writer.push_key "key2"
writer.push_value "value2"
writer.pop
writer.output
end
benchmark("Nested object") do
writer = Panko::ObjectWriter.new
writer.push_object
writer.push_value "value1", "key1"
writer.push_object "key2"
writer.push_value "value2", "key2"
writer.pop
writer.pop
writer.output
end
================================================
FILE: benchmarks/panko_json.rb
================================================
# frozen_string_literal: true
require_relative "support/datasets"
class AuthorFastSerializer < Panko::Serializer
attributes :id, :name
end
class PostFastSerializer < Panko::Serializer
attributes :id, :body, :title, :author_id, :created_at
end
class PostFastWithMethodCallSerializer < Panko::Serializer
attributes :id, :body, :title, :author_id, :method_call
def method_call
object.id * 2
end
end
class PostFastWithJsonSerializer < Panko::Serializer
attributes :id, :body, :title, :author_id, :created_at, :data
end
class PostWithHasOneFastSerializer < Panko::Serializer
attributes :id, :body, :title, :author_id, :created_at
has_one :author, serializer: AuthorFastSerializer
end
class AuthorWithHasManyFastSerializer < Panko::Serializer
attributes :id, :name
has_many :posts, serializer: PostFastSerializer
end
class PostWithAliasFastSerializer < Panko::Serializer
attributes :new_id, :new_body, :new_title, :new_author_id, :new_created_at
end
benchmark_with_records("Simple", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostFastSerializer).to_json }
benchmark_with_records("HasOne", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithHasOneFastSerializer).to_json }
benchmark_with_records("HasMany", type: :authors) { |r| Panko::ArraySerializer.new(r, each_serializer: AuthorWithHasManyFastSerializer).to_json }
benchmark_with_records("MethodCall", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostFastWithMethodCallSerializer).to_json }
benchmark_with_records("JSON column", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostFastWithJsonSerializer).to_json }
benchmark_with_records("Except", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithHasOneFastSerializer, except: [:title]).to_json }
benchmark_with_records("Only", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithHasOneFastSerializer, only: [:id, :body, :author_id, :author]).to_json }
benchmark_with_records("Aliases", type: :aliased_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithAliasFastSerializer).to_json }
================================================
FILE: benchmarks/panko_object.rb
================================================
# frozen_string_literal: true
require_relative "support/datasets"
class AuthorFastSerializer < Panko::Serializer
attributes :id, :name
end
class PostFastSerializer < Panko::Serializer
attributes :id, :body, :title, :author_id, :created_at
end
class PostWithHasOneFastSerializer < Panko::Serializer
attributes :id, :body, :title, :author_id, :created_at
has_one :author, serializer: AuthorFastSerializer
end
class PostWithAliasFastSerializer < Panko::Serializer
attributes :new_id, :new_body, :new_title, :new_author_id, :new_created_at
end
benchmark_with_records("Simple", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostFastSerializer).to_a }
benchmark_with_records("HasOne", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithHasOneFastSerializer).to_a }
benchmark_with_records("Except", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithHasOneFastSerializer, except: [:title]).to_a }
benchmark_with_records("Only", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithHasOneFastSerializer, only: [:id, :body, :author_id, :author]).to_a }
benchmark_with_records("Aliases", type: :aliased_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithAliasFastSerializer).to_a }
================================================
FILE: benchmarks/plain_object.rb
================================================
# frozen_string_literal: true
require_relative "support/datasets"
class PlainAuthorSerializer < Panko::Serializer
attributes :id, :name
end
class PlainPostSerializer < Panko::Serializer
attributes :id, :body, :title, :author_id, :created_at
end
class PlainPostWithMethodCallSerializer < Panko::Serializer
attributes :id, :body, :title, :author_id, :method_call
def method_call
object.id * 2
end
end
class PlainPostWithHasOneSerializer < Panko::Serializer
attributes :id, :body, :title, :author_id, :created_at
has_one :author, serializer: PlainAuthorSerializer
end
benchmark_with_records("Simple", type: :plain_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PlainPostSerializer).to_json }
benchmark_with_records("HasOne", type: :plain_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PlainPostWithHasOneSerializer).to_json }
benchmark_with_records("MethodCall", type: :plain_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PlainPostWithMethodCallSerializer).to_json }
benchmark_with_records("Except", type: :plain_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PlainPostWithHasOneSerializer, except: [:title]).to_json }
benchmark_with_records("Only", type: :plain_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PlainPostWithHasOneSerializer, only: [:id, :body, :author_id, :author]).to_json }
================================================
FILE: benchmarks/support/benchmark.rb
================================================
# frozen_string_literal: true
require "bundler/setup"
require "benchmark/ips"
require "memory_profiler"
require "active_support/all"
# Enable YJIT if available (Ruby 3.1+)
RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
# Sizes to benchmark. Override with SIZE=n env var (single run).
# @return [Array<Integer>]
BENCHMARK_SIZES = ENV["SIZE"] ? [Integer(ENV["SIZE"])] : [50, 2300]
# Registry of pre-loaded dataset slices, keyed by type symbol.
# Populated by support/datasets.rb before any benchmark file runs.
# @return [Hash{Symbol => Hash}]
DATASETS = {}
# Benchmark.ips measurement time in seconds (default 10).
# @return [Integer]
IPS_TIME = Integer(ENV.fetch("IPS_TIME", 10))
# Benchmark.ips warmup time in seconds (default 3).
# @return [Integer]
IPS_WARMUP = Integer(ENV.fetch("IPS_WARMUP", 3))
# ---------------------------------------------------------------------------
# NoopWriter
# ---------------------------------------------------------------------------
# A no-op Oj::StringWriter stand-in used by benchmarks that test
# serialization logic without paying JSON-string allocation costs.
class NoopWriter
# The last value passed to push_value.
# @return [Object, nil]
attr_reader :value
# Records +value+ without writing JSON.
#
# @param value [Object] the value to (not) write
# @param key [String, nil] ignored
# @return [void]
def push_value(value, key = nil)
@value = value
end
# No-op JSON push.
#
# @param value [String] ignored
# @param key [String, nil] ignored
# @return [nil]
def push_json(value, key = nil) # rubocop:disable Lint/UnusedMethodArgument
nil
end
end
# ---------------------------------------------------------------------------
# Internal state
# ---------------------------------------------------------------------------
# @!visibility private
@header_printed = false
# @!visibility private
@profile_blocks = []
# ---------------------------------------------------------------------------
# print_header
# ---------------------------------------------------------------------------
# Prints the benchmark table header once, deriving the section title from
# the calling file's basename (without extension).
#
# @return [void]
def print_header
return if @header_printed
@header_printed = true
title = File.basename($PROGRAM_NAME, ".*")
width = 78
puts "=" * width
puts " #{title}".center(width)
puts "=" * width
puts "benchmark ips allocs retained"
puts "-" * width
end
# ---------------------------------------------------------------------------
# benchmark
# ---------------------------------------------------------------------------
# Runs a single benchmark case, printing one formatted result row.
#
# Respects the BENCH env var: if set, only runs benchmarks whose +label+
# contains the value as a case-insensitive substring.
#
# When PROFILE=cpu : collects the block for a single StackProf run at exit.
# When PROFILE=memory: runs MemoryProfiler and calls pretty_print immediately.
# Normal mode : disables GC, measures allocations + ips, prints a row.
#
# @param label [String] human-readable name shown in the output table
# @yield the code under measurement (called many times by Benchmark.ips)
# @return [void]
def benchmark(label, &block)
filter = ENV["BENCH"]
return if filter && !label.downcase.include?(filter.downcase)
print_header
case ENV["PROFILE"]
when "cpu"
@profile_blocks << [label, block]
return
when "memory"
report = MemoryProfiler.report(&block)
report.pretty_print
return
end
GC.start
GC.disable
memory_report = MemoryProfiler.report(&block)
ips_result = Benchmark.ips(IPS_TIME, IPS_WARMUP, true) do |x|
x.report(label, &block)
end
GC.enable
ips = ips_result.entries.first.ips.round(2)
allocs = memory_report.total_allocated
retained = memory_report.total_retained
ips_str = format("%.2f", ips).reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse
allocs_str = allocs.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse
retained_str = retained.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1,').reverse
puts format("%-54s %12s %10s %10s", label, ips_str, allocs_str, retained_str)
end
# ---------------------------------------------------------------------------
# benchmark_with_records
# ---------------------------------------------------------------------------
# Iterates BENCHMARK_SIZES and runs one benchmark per size, automatically
# slicing the dataset registered under +type+.
#
# @param label [String] benchmark label prefix (size + noun are appended)
# @param type [Symbol] key into DATASETS (e.g. :posts)
# @yield [records] the subset of records for this size
# @yieldparam records [Array] first +n+ records from the dataset
# @return [void]
def benchmark_with_records(label, type:, &block)
dataset = DATASETS.fetch(type)
data = dataset[:data]
noun = dataset[:noun]
BENCHMARK_SIZES.each do |n|
subset = data.first(n)
benchmark("#{label}, #{n} #{noun}") { block.call(subset) }
end
end
# ---------------------------------------------------------------------------
# run_cpu_profile
# ---------------------------------------------------------------------------
# Runs all blocks collected in PROFILE=cpu mode under a single StackProf
# session and prints the results.
#
# @return [void]
def run_cpu_profile
return if @profile_blocks.empty?
require "stackprof"
combined = @profile_blocks.map { |_lbl, blk| blk }
profile = StackProf.run(mode: :cpu, raw: true) do
combined.each(&:call)
end
StackProf::Report.new(profile).print_text
end
# ---------------------------------------------------------------------------
# at_exit
# ---------------------------------------------------------------------------
at_exit do
run_cpu_profile if ENV["PROFILE"] == "cpu"
puts "=" * 78 if @header_printed
end
================================================
FILE: benchmarks/support/datasets.rb
================================================
# frozen_string_literal: true
require_relative "benchmark"
require_relative "setup"
require "panko_serializer"
# --- AR datasets ---
DATASETS[:posts] = {data: Post.all.includes(:author).to_a, noun: "posts"}
DATASETS[:authors] = {data: Author.all.includes(:posts).to_a, noun: "authors"}
DATASETS[:aliased_posts] = {data: PostWithAliasModel.all.to_a, noun: "aliased posts"}
# --- Plain Ruby datasets (no AR dependency) ---
class PlainAuthor
attr_accessor :id, :name
end
class PlainPost
attr_accessor :id, :body, :title, :created_at, :author_id
attr_reader :author
def author=(author)
@author = author
@author_id = author.id
end
end
plain_posts = 2300.times.map do |i|
author = PlainAuthor.new
author.id = i
author.name = "Author #{i}"
post = PlainPost.new
post.id = i
post.body = "something about how password restrictions are evil"
post.title = "Your bank does not know how to do security"
post.created_at = Time.now
post.author = author
post
end
DATASETS[:plain_posts] = {data: plain_posts, noun: "plain posts"}
================================================
FILE: benchmarks/support/setup.rb
================================================
# frozen_string_literal: true
require "active_record"
require "sqlite3"
require "securerandom"
# Change the following to reflect your database settings
ActiveRecord::Base.establish_connection(
adapter: "sqlite3",
database: ":memory:"
)
# Don't show migration output when constructing fake db
ActiveRecord::Migration.verbose = false
ActiveRecord::Schema.define do
create_table :authors, force: true do |t|
t.string :name
t.timestamps(null: false)
end
create_table :posts, force: true do |t|
t.text :body
t.string :title
t.references :author
t.json :data
t.timestamps(null: false)
end
end
class Author < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :author
end
class PostWithAliasModel < ActiveRecord::Base
self.table_name = "posts"
alias_attribute :new_id, :id
alias_attribute :new_body, :body
alias_attribute :new_title, :title
alias_attribute :new_author_id, :author_id
alias_attribute :new_created_at, :created_at
end
Post.transaction do
2300.times do
Post.create(
body: SecureRandom.hex(30),
title: SecureRandom.hex(20),
author: Author.create(name: SecureRandom.alphanumeric),
data: {a: 1, b: 2, c: 3}
)
end
end
================================================
FILE: benchmarks/type_casts/generic.rb
================================================
# frozen_string_literal: true
require_relative "../support/benchmark"
require "active_record"
require "panko_serializer"
Time.zone = "UTC"
def bench_type(type_klass, from, to, label: type_klass.name)
converter = type_klass.new
benchmark("#{label} TypeCast") do
Panko._type_cast(converter, from)
end
benchmark("#{label} NoTypeCast") do
Panko._type_cast(converter, to)
end
end
bench_type ActiveRecord::Type::String, 1, "1"
bench_type ActiveRecord::Type::Text, 1, "1"
bench_type ActiveRecord::Type::ImmutableString, 1, "1"
bench_type ActiveRecord::Type::Integer, "1", 1
bench_type ActiveRecord::Type::BigInteger, "1", 1
bench_type ActiveRecord::Type::Float, "1.23", 1.23
bench_type ActiveRecord::Type::Decimal, "123.45", BigDecimal("123.45")
bench_type ActiveRecord::Type::Boolean, "true", true
bench_type ActiveRecord::Type::Boolean, "t", true, label: "ActiveRecord::Type::Boolean(t)"
bench_type ActiveRecord::Type::Date, "2017-03-04", Date.new(2017, 3, 4)
bench_type ActiveRecord::Type::Time, "2000-01-01 12:45:23", Time.utc(2000, 1, 1, 12, 45, 23)
bench_type ActiveRecord::Type::DateTime, "2017-03-04 12:45:23", Time.utc(2017, 3, 4, 12, 45, 23)
bench_type ActiveRecord::Type::Binary, "data", "data".b
if defined?(ActiveRecord::Type::Json)
bench_type ActiveRecord::Type::Json, '{"a":1}', {"a" => 1}
end
================================================
FILE: benchmarks/type_casts/mysql.rb
================================================
# frozen_string_literal: true
require_relative "../support/benchmark"
require "active_record"
require "panko_serializer"
begin
require "active_record/connection_adapters/mysql2_adapter"
rescue LoadError
begin
require "active_record/connection_adapters/trilogy_adapter"
rescue LoadError
puts "Skipping MySQL type_casts: mysql2/trilogy gem not installed"
exit 0
end
end
def bench_type(type_klass, from, to, label: type_klass.name)
converter = type_klass.new
benchmark("#{label} TypeCast") do
Panko._type_cast(converter, from)
end
benchmark("#{label} NoTypeCast") do
Panko._type_cast(converter, to)
end
end
def bench_type_with_instance(instance, from, to, label:)
benchmark("#{label} TypeCast") do
Panko._type_cast(instance, from)
end
benchmark("#{label} NoTypeCast") do
Panko._type_cast(instance, to)
end
end
# --- MySQL-specific: UnsignedInteger ---
if defined?(ActiveRecord::Type::UnsignedInteger)
bench_type ActiveRecord::Type::UnsignedInteger, "42", 42
end
# --- MySQL String with boolean coercion (true: "1", false: "0") ---
mysql_string = ActiveModel::Type::String.new(true: "1", false: "0") # rubocop:disable Lint/BooleanSymbol
bench_type_with_instance(mysql_string, 1, "1", label: "MySQL::String (bool coercion)")
mysql_immutable = ActiveModel::Type::ImmutableString.new(true: "1", false: "0") # rubocop:disable Lint/BooleanSymbol
bench_type_with_instance(mysql_immutable, 1, "1", label: "MySQL::ImmutableString (bool coercion)")
# --- MySQL Text size variants ---
bench_type_with_instance(ActiveRecord::Type::Text.new(limit: 2**8 - 1), 1, "1", label: "MySQL::TinyText")
bench_type_with_instance(ActiveRecord::Type::Text.new(limit: 2**16 - 1), 1, "1", label: "MySQL::Text")
bench_type_with_instance(ActiveRecord::Type::Text.new(limit: 2**24 - 1), 1, "1", label: "MySQL::MediumText")
bench_type_with_instance(ActiveRecord::Type::Text.new(limit: 2**32 - 1), 1, "1", label: "MySQL::LongText")
# --- MySQL Binary size variants ---
bench_type_with_instance(ActiveModel::Type::Binary.new(limit: 2**8 - 1), "data", "data".b, label: "MySQL::TinyBlob")
bench_type_with_instance(ActiveModel::Type::Binary.new(limit: 2**16 - 1), "data", "data".b, label: "MySQL::Blob")
bench_type_with_instance(ActiveModel::Type::Binary.new(limit: 2**24 - 1), "data", "data".b, label: "MySQL::MediumBlob")
bench_type_with_instance(ActiveModel::Type::Binary.new(limit: 2**32 - 1), "data", "data".b, label: "MySQL::LongBlob")
# --- MySQL Float variants ---
bench_type_with_instance(ActiveModel::Type::Float.new(limit: 24), "1.23", 1.23, label: "MySQL::Float (single)")
bench_type_with_instance(ActiveModel::Type::Float.new(limit: 53), "1.23", 1.23, label: "MySQL::Double (double)")
# --- MySQL Boolean (via tinyint(1) emulation) ---
bench_type_with_instance(ActiveModel::Type::Boolean.new, 1, true, label: "MySQL::Boolean (tinyint)")
================================================
FILE: benchmarks/type_casts/postgresql.rb
================================================
# frozen_string_literal: true
require_relative "../support/benchmark"
require "active_record"
require "panko_serializer"
begin
require "pg"
require "active_record/connection_adapters/postgresql_adapter"
rescue LoadError
puts "Skipping PostgreSQL type_casts: pg gem not installed"
exit 0
end
Time.zone = "UTC"
def bench_type(type_klass, from, to, label: type_klass.name)
converter = type_klass.new
benchmark("#{label} TypeCast") do
Panko._type_cast(converter, from)
end
benchmark("#{label} NoTypeCast") do
Panko._type_cast(converter, to)
end
end
PG_OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
bench_type PG_OID::Uuid, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
if defined?(PG_OID::Jsonb)
bench_type PG_OID::Jsonb, '{"a":1}', {"a" => 1}
end
if defined?(PG_OID::Hstore)
bench_type PG_OID::Hstore, '"a"=>"1"', {"a" => "1"}
end
if defined?(PG_OID::Inet)
bench_type PG_OID::Inet, "192.168.1.1", IPAddr.new("192.168.1.1")
end
if defined?(PG_OID::Cidr)
bench_type PG_OID::Cidr, "192.168.1.0/24", IPAddr.new("192.168.1.0/24")
end
if defined?(PG_OID::Macaddr)
bench_type PG_OID::Macaddr, "00:11:22:33:44:55", "00:11:22:33:44:55"
end
if defined?(PG_OID::Point)
bench_type PG_OID::Point, "(1.0,2.0)", [1.0, 2.0]
end
if defined?(PG_OID::Money)
bench_type PG_OID::Money, "$1,234.56", BigDecimal("1234.56")
end
if defined?(PG_OID::Bit)
bench_type PG_OID::Bit, "101", "101"
end
if defined?(PG_OID::BitVarying)
bench_type PG_OID::BitVarying, "101", "101"
end
if defined?(PG_OID::Xml)
bench_type PG_OID::Xml, "<a/>", "<a/>"
end
if defined?(PG_OID::Enum)
bench_type PG_OID::Enum, "active", "active"
end
if defined?(PG_OID::DateTime)
tz_type = PG_OID::DateTime.new
tz_converter = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(tz_type)
benchmark("PG DateTime+TZ TypeCast") do
Panko._type_cast(tz_converter, "2017-07-10 09:26:40.937392")
end
end
================================================
FILE: benchmarks/type_casts/sqlite.rb
================================================
# frozen_string_literal: true
require_relative "../support/benchmark"
require "active_record"
require "sqlite3"
require "panko_serializer"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
sqlite_int_type = ActiveRecord::Type::Integer.new(limit: 8)
benchmark("SQLite3 Integer(limit:8) TypeCast") do
Panko._type_cast(sqlite_int_type, "42")
end
benchmark("SQLite3 Integer(limit:8) NoTypeCast") do
Panko._type_cast(sqlite_int_type, 42)
end
================================================
FILE: docs/CNAME
================================================
panko.dev
================================================
FILE: docs/Gemfile
================================================
# frozen_string_literal: true
source "https://rubygems.org"
gem "just-the-docs"
================================================
FILE: docs/_config.yml
================================================
title: Panko Serializers
description: High Performance JSON Serialization for ActiveRecord & Ruby Objects
url: https://panko.dev
baseurl: ""
theme: just-the-docs
enable_copy_code_button: true
nav_external_links:
- title: GitHub
url: https://github.com/yosiat/panko_serializer
footer_content: >-
<iframe src="https://ghbtns.com/github-btn.html?user=yosiat&repo=panko_serializer&type=star&count=true&size=medium"
frameborder="0" scrolling="0" width="150" height="20" title="GitHub Stars"></iframe>
<br>
Copyright © 2026 Panko Serializer.
Distributed under the
<a href="https://github.com/yosiat/panko_serializer/blob/master/LICENSE.txt">MIT License</a>.
exclude:
- Gemfile
- Gemfile.lock
================================================
FILE: docs/associations.md
================================================
---
title: Associations
layout: default
nav_order: 6
parent: Reference
---
# Associations
A serializer can define it's own associations - both `has_many` and `has_one` to serialize under the context of the object.
For example:
```ruby
class PostSerializer < Panko::Serializer
attributes :title, :body
has_one :author, serializer: AuthorSerializer
has_many :comments, each_serializer: CommentSerializer
end
```
### Associations with aliases
An association key name can be aliased with the `name` option.
For example:
the `actual_author` property will be converted to `alias_author`.
```ruby
class PostSerializer < Panko::Serializer
attributes :title, :body
has_one :actual_author, serializer: AuthorSerializer, name: :alias_author
has_many :comments, each_serializer: CommentSerializer
end
```
### Inference
Panko can find the type of the serializer by looking at the relationship name, so instead of specifying
the serializer at the above example, we can:
```ruby
class PostSerializer < Panko::Serializer
attributes :title, :body
has_one :author
has_many :comments
end
```
The logic of inferencing is:
- Take the name of the relationship (for example - `:author` / `:comments`) singularize and camelize it.
- Look for const defined with the name above and "Serializer" suffix (by using `Object.const_get`).
> 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`.
## Nested Filters
As talked before, Panko allows you to filter the attributes of a serializer.
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.
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.
We can declare tailored serializer for this, or we can re-use the above defined serializer - `PostSerializer` and use nested filters.
```ruby
posts = Post.all
Panko::ArraySerializer.new(posts, each_serializer: PostSerializer, only: {
instance: [:title, :body, :author, :comments],
author: [:id],
comments: [:id],
})
```
Let's dissect the `only` option we passed:
- `instance` - list of attributes (and associations) we want to serialize for the current instance of the serializer, in this case - `PostSerializer`.
- `author`, `comments` - here we specify the list of attributes we want to serialize for each association.
It's important to note that Nested Filters are recursive, in other words, we can filter the association's associations.
For example, `CommentSerializer` has an `has_one` association `Author`, and for each `comments.author` we can only serialize it's name.
```ruby
posts = Post.all
Panko::ArraySerializer.new(posts, only: {
instance: [:title, :body, :author, :comments],
author: [:id],
comments: {
instance: [:id, :author],
author: [:name]
}
})
```
As you see now in `comments` the `instance` have different meaning, the `CommentSerializer`.
================================================
FILE: docs/attributes.md
================================================
---
title: Attributes
layout: default
nav_order: 5
parent: Reference
---
# Attributes
Attributes allow you to specify which record attributes you want to serialize.
There are two types of attributes:
- Field - simple columns defined on the record it self.
- Virtual/Method - this allows to include properties beyond simple fields.
```ruby
class UserSerializer < Panko::Serializer
attributes :full_name
def full_name
"#{object.first_name} #{object.last_name}"
end
end
```
## Field Attributes
Using field attributes you can control which columns of the given ActiveRecord object you want to serialize.
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]({% link design-choices.md %}#type-casting)).
## Method Attributes
Method attributes are used when your serialized values can be derived from the object you are serializing.
The serializer's attribute methods can access the object being serialized as `object`:
```ruby
class PostSerializer < Panko::Serializer
attributes :author_name
def author_name
"#{object.author.first_name} #{object.author.last_name}"
end
end
```
Another useful thing you can pass your serializer is `context`, a `context` is a bag of data whom your serializer may need.
For example, here we will pass feature flags:
```ruby
class UserSerializer < Panko::Serializer
attributes :id, :email
def feature_flags
context[:feature_flags]
end
end
serializer = UserSerializer.new(context: {
feature_flags: FeatureFlags.all
})
serializer.serialize(User.first)
```
## Filters
Filters allows us to reduce the amount of attributes we can serialize, therefore reduce the data usage & performance of serializing.
There are two types of filters:
- only - use those attributes **only** and nothing else.
- except - all attributes **except** those attributes.
Usage example:
```ruby
class UserSerializer < Panko::Serializer
attributes :id, :name, :email
end
# this line will return { 'name': '..' }
UserSerializer.new(only: [:name]).serialize(User.first)
# this line will return { 'id': '..', 'email': ... }
UserSerializer.new(except: [:name]).serialize(User.first)
```
> **Note** that if you want to user filter on an associations, the `:name` property is not taken into account.
If you have a `has_many :state_transitions, name: :history` association defined, the key to use in filters is
`:state_transitions` (e.g. `{ except: [:state_transitions] }`).
## Filters For
Sometimes you find yourself having the same filtering logic in actions. In order to
solve this duplication, Panko allows you to write the filters in the serializer.
```ruby
class UserSerializer < Panko::Serializer
attributes :id, :name, :email
def self.filters_for(context, scope)
{
only: [:name]
}
end
end
# this line will return { 'name': '..' }
UserSerializer.serialize(User.first)
```
> See discussion in: [https://github.com/yosiat/panko_serializer/issues/16](https://github.com/yosiat/panko_serializer/issues/16)
## Aliases
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:
```ruby
class PostSerializer < Panko::Serializer
attributes :published_at
def published_at
object.created_at
end
end
```
The downside of this approach is that `created_at` skips Panko's type casting, therefore we get a direct hit on performance.
To fix this, we can use aliases:
```ruby
class PostSerializer < Panko::Serializer
aliases created_at: :published_at
end
```
================================================
FILE: docs/design-choices.md
================================================
---
title: Design Choices
layout: default
nav_order: 4
---
# Design Choices
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).
Its performance is achieved by:
- `Oj::StringWriter` - I will elaborate later.
- Type casting — instead of relying on ActiveRecord to do its type cast, Panko is doing it by itself.
- Figuring out the metadata, ahead of time — therefore, we ask less questions during the `serialization loop`.
## Serialization overview
First, let's start with an overview. Let's say we want to serialize an `User` object, which has
`first_name`, `last_name`, `age`, and `email` properties.
The serializer definition will be something like this:
```ruby
class UserSerializer < Panko::Serializer
attributes :name, :age, :email
def name
"#{object.first_name} #{object.last_name}"
end
end
```
And the usage of this serializer will be:
```ruby
# fetch user from database
user = User.first
# create serializer, with empty options
serializer = UserSerializer.new
# serialize to JSON
serializer.serialize_to_json(user)
```
Let's go over the steps that Panko will execute behind the scenes for this flow.
_I will skip the serializer definition part, because it's fairly simple and straightforward (see `lib/panko/serializer.rb`)._
First step, while initializing the UserSerializer, we will create a **Serialization Descriptor** for this class.
Serialization Descriptor's goal is to answer those questions:
- Which fields do we have? In our case, `:age`, `:email`.
- Which method fields do we have? In our case `:name`.
- Which associations do we have (and their serialization descriptors)?
The serialization description is also responsible for filtering the attributes (`only` \\ `except`).
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.
In C land, we take the `user` object and the serialization descriptor, and start the serialization process which is separated to 4 parts:
- Serializing Fields - looping through serialization descriptor's `fields` and read them from the ActiveRecord object (see `Type Casting`) and write them to the writer.
- Serializing Method Fields - creating (a cached) serializer instance, setting its `@object` and `@context`, calling all the method fields and writing them to the writer.
- Serializing associations — this is simple, once we have fields + method fields, we just repeat the process.
Once this is finished, we have a nice JSON string.
Now let's dig deeper.
## Interesting parts
### Oj::StringWriter
If you read the code of ActiveRecord serialization code in Ruby, you will observe this flow:
1. Get an array of ActiveRecord objects (`User.all` for example).
2. Build a new array of hashes where each hash is an `User` with the attributes we selected.
3. The JSON serializer, takes this array of hashes and loop them, and converts it to a JSON string.
This entire process is expensive in terms of Memory & CPU, and this where the combination of Panko and Oj::StringWriter really shines.
In Panko, the serialization process of the above is:
1. Get an array of ActiveRecord objects (`User.all` for example).
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.
3. Get from `Oj::StringWriter` the completed JSON string — which is a no-op, since `Oj::StringWriter` already built the string.
### Figuring out the metadata, ahead of time.
Another observation I noticed in the Ruby serializers is that they ask and do a lot in a serialization loop:
- Is this field a method? is it a property?
- Which fields and associations do I need for the serializer to consider the `only` and `except` options?
- What is the serializer of this has_one association?
Panko tries to ask the bare minimum in serialization by building `Serialization Descriptor` for each serialization and caching it.
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`).
### Type Casting
This is the final part, which helped yield most of the performance improvements.
In ActiveRecord, when we read the value of an attribute, it does type casting of the DB value to its real Ruby type.
For example, time strings are converted to Time objects, Strings are duplicated, and Integers are converted from their values to Number.
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".
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.
What Panko does is that if we have ActiveRecord type string, we won't duplicate it.
If we have an integer string value, we will convert it to an integer, and the same goes for other types.
All of these conversions are done in C, which of course yields a big performance improvement.
#### Time type casting
While you read Panko source code, you will encounter the time type casting and immediately you will have a "WTF?" moment.
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.
The time type casting works as follows:
- If it's a string that ends with `Z`, and the strings matches the UTC ISO8601 regex, then we just return the string.
- 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).
- If it's none of the above, I will let ActiveRecord type casting do it's magic.
================================================
FILE: docs/getting-started.md
================================================
---
title: Getting Started
layout: default
nav_order: 2
---
# Getting Started
## Installation
To install Panko, all you need is to add it to your Gemfile:
```ruby
gem "panko_serializer"
```
Then, install it on the command line:
```
bundle install
```
## Creating your first serializer
Let's create a serializer and use it inside of a Rails controller:
```ruby
class PostSerializer < Panko::Serializer
attributes :title
end
class UserSerializer < Panko::Serializer
attributes :id, :name, :age
has_many :posts, serializer: PostSerializer
end
```
### Serializing an object
And now serialize a single object:
```ruby
# Using Oj serializer
PostSerializer.new.serialize_to_json(Post.first)
# or, similar to #serializable_hash
PostSerializer.new.serialize(Post.first).to_json
```
### Using the serializers in a controller
As you can see, defining serializers is simple and resembles ActiveModelSerializers 0.9.
To utilize the `UserSerializer` inside a Rails controller and serialize some users, all we need to do is:
```ruby
class UsersController < ApplicationController
def index
users = User.includes(:posts).all
render json: Panko::ArraySerializer.new(users, each_serializer: UserSerializer).to_json
end
end
```
And voila, we have an endpoint which serializes users using Panko!
================================================
FILE: docs/index.md
================================================
---
title: Introduction
layout: default
nav_order: 1
---
# Introduction
Panko is a library which is inspired by ActiveModelSerializers 0.9 for serializing ActiveRecord/Ruby objects to JSON strings, fast.
To achieve it's [performance]({% link performance.md %}):
- Oj - Panko relies on Oj since it's fast and allow to serialize incrementally using `Oj::StringWriter`.
- Serialization Descriptor - Panko computes most of the metadata ahead of time, to save time later in serialization.
- Type casting — Panko does type casting by itself, instead of relying on ActiveRecord.
================================================
FILE: docs/performance.md
================================================
---
title: Performance
layout: default
nav_order: 3
---
# Performance
The performance of Panko is measured using microbenchmarks and load testing.
## Microbenchmarks
The following microbenchmarks are run on MacBook Pro (16-inch, 2021, M1 Max), Ruby 3.2.0 with Rails 7.0.5
demonstrating the performance of ActiveModelSerializers 0.10.13 and Panko 0.8.0.
| Benchmark | AMS ip/s | Panko ip/s |
| ----------------- | -------- | ---------- |
| Simple_Posts_2300 | 11.72 | 523.05 |
| Simple_Posts_50 | 557.29 | 23,011.9 |
| HasOne_Posts_2300 | 5.91 | 233.44 |
| HasOne_Posts_50 | 285.8 | 10,362.79 |
## Real-world benchmark
The real-world benchmark here is an endpoint which serializes 7,884 entries with 48 attributes and no associations.
The benchmark took place in an environment that simulates production environment and run using `wrk` from machine on the same cluster.
| Metric | AMS | Panko |
| ------------------ | ----- | ----- |
| Avg Response Time | 4.89s | 1.48s |
| Max Response Time | 5.42s | 1.83s |
| 99th Response Time | 5.42s | 1.74s |
| Total Requests | 61 | 202 |
_Thanks to [Bringg](https://www.bringg.com) for providing the infrastructure for the benchmarks._
================================================
FILE: docs/reference.md
================================================
---
title: Reference
layout: default
nav_order: 5
has_children: true
---
# Reference
Detailed reference documentation for Panko Serializer's features.
================================================
FILE: docs/response-bag.md
================================================
---
title: Response
layout: default
nav_order: 7
parent: Reference
---
# Response
Let's say you have some JSON payload which is constructed using Panko serialization result,
like this:
```ruby
class PostsController < ApplicationController
def index
posts = Post.all
render json: {
success: true,
total_count: posts.count,
posts: Panko::ArraySerializer.new(posts, each_serializer: PostSerializer).to_json
}
end
end
```
The output of the above will be a JSON string (for `posts`) inside a JSON string and this were `Panko::Response` shines.
```ruby
class PostsController < ApplicationController
def index
posts = Post.all
render json: Panko::Response.new(
success: true,
total_count: posts.count,
posts: Panko::ArraySerializer.new(posts, each_serializer: PostSerializer)
)
end
end
```
And everything will work as expected!
For a single object serialization, we need to use a different API (since `Panko::Serializer` doesn't accept an object in it's constructor):
```ruby
class PostsController < ApplicationController
def show
post = Post.find(params[:id])
render(
json: Panko::Response.create do |r|
{
success: true,
post: r.serializer(post, PostSerializer)
}
end
)
end
end
```
## JsonValue
Let's take the above example further, we will serialize the posts and cache it as JSON string in our Cache.
Now, you can wrap the cached value with `Panko::JsonValue`, like here:
```ruby
class PostsController < ApplicationController
def index
posts = Cache.get("/posts")
render json: Panko::Response.new(
success: true,
total_count: posts.count,
posts: Panko::JsonValue.from(posts)
)
end
end
```
================================================
FILE: ext/panko_serializer/attributes_writer/active_record.c
================================================
#include "active_record.h"
static ID attributes_id;
static ID types_id;
static ID additional_types_id;
static ID values_id;
static ID delegate_hash_id;
static ID value_before_type_cast_id;
static ID type_id;
static ID fetch_id;
struct attributes {
// Hash
VALUE attributes_hash;
size_t attributes_hash_size;
// Hash
VALUE types;
// Hash
VALUE additional_types;
// heuristics
bool tryToReadFromAdditionalTypes;
// Rails <8: Hash
// Rails >=8: ActiveRecord::Result::IndexedRow
VALUE values;
// Hash
VALUE indexed_row_column_indexes;
// Array or NIL
VALUE indexed_row_row;
bool is_indexed_row;
};
struct attributes init_context(VALUE obj) {
volatile VALUE attributes_set = rb_ivar_get(obj, attributes_id);
volatile VALUE attributes_hash = rb_ivar_get(attributes_set, attributes_id);
struct attributes attrs = (struct attributes){
.attributes_hash =
PANKO_EMPTY_HASH(attributes_hash) ? Qnil : attributes_hash,
.attributes_hash_size = 0,
.types = rb_ivar_get(attributes_set, types_id),
.additional_types = rb_ivar_get(attributes_set, additional_types_id),
.tryToReadFromAdditionalTypes =
PANKO_EMPTY_HASH(rb_ivar_get(attributes_set, additional_types_id)) ==
false,
.values = rb_ivar_get(attributes_set, values_id),
.is_indexed_row = false,
.indexed_row_column_indexes = Qnil,
.indexed_row_row = Qnil,
};
if (attrs.attributes_hash != Qnil) {
attrs.attributes_hash_size = RHASH_SIZE(attrs.attributes_hash);
}
if (strcmp(rb_class2name(CLASS_OF(attrs.values)),
"ActiveRecord::Result::IndexedRow") == 0) {
volatile VALUE indexed_row_column_indexes =
rb_ivar_get(attrs.values, rb_intern("@column_indexes"));
volatile VALUE indexed_row_row =
rb_ivar_get(attrs.values, rb_intern("@row"));
attrs.indexed_row_column_indexes = indexed_row_column_indexes;
attrs.indexed_row_row = indexed_row_row;
attrs.is_indexed_row = true;
}
return attrs;
}
VALUE _read_value_from_indexed_row(struct attributes attributes_ctx,
volatile VALUE member) {
volatile VALUE value = Qnil;
if (NIL_P(attributes_ctx.indexed_row_column_indexes) ||
NIL_P(attributes_ctx.indexed_row_row)) {
return value;
}
volatile VALUE column_index =
rb_hash_aref(attributes_ctx.indexed_row_column_indexes, member);
if (NIL_P(column_index)) {
return value;
}
volatile VALUE row = attributes_ctx.indexed_row_row;
if (NIL_P(row)) {
return value;
}
return RARRAY_AREF(row, NUM2INT(column_index));
}
VALUE read_attribute(struct attributes attributes_ctx, Attribute attribute,
volatile VALUE* isJson) {
volatile VALUE member, value;
member = attribute->name_str;
value = Qnil;
if (
// we have attributes_hash
!NIL_P(attributes_ctx.attributes_hash)
// It's not empty
&& (attributes_ctx.attributes_hash_size > 0)) {
volatile VALUE attribute_metadata =
rb_hash_aref(attributes_ctx.attributes_hash, member);
if (attribute_metadata != Qnil) {
value = rb_ivar_get(attribute_metadata, value_before_type_cast_id);
if (NIL_P(attribute->type)) {
attribute->type = rb_ivar_get(attribute_metadata, type_id);
}
}
}
if (NIL_P(value) && !NIL_P(attributes_ctx.values)) {
if (attributes_ctx.is_indexed_row == true) {
value = _read_value_from_indexed_row(attributes_ctx, member);
} else {
value = rb_hash_aref(attributes_ctx.values, member);
}
}
if (NIL_P(attribute->type) && !NIL_P(value)) {
if (attributes_ctx.tryToReadFromAdditionalTypes == true) {
attribute->type = rb_hash_aref(attributes_ctx.additional_types, member);
}
if (!NIL_P(attributes_ctx.types) && NIL_P(attribute->type)) {
attribute->type = rb_hash_aref(attributes_ctx.types, member);
}
}
if (!NIL_P(attribute->type) && !NIL_P(value)) {
return type_cast(attribute->type, value, isJson);
}
return value;
}
void active_record_attributes_writer(VALUE obj, VALUE attributes,
EachAttributeFunc write_value,
VALUE writer) {
long i;
struct attributes attributes_ctx = init_context(obj);
volatile VALUE record_class = CLASS_OF(obj);
for (i = 0; i < RARRAY_LEN(attributes); i++) {
volatile VALUE raw_attribute = RARRAY_AREF(attributes, i);
Attribute attribute = PANKO_ATTRIBUTE_READ(raw_attribute);
attribute_try_invalidate(attribute, record_class);
volatile VALUE isJson = Qfalse;
volatile VALUE value = read_attribute(attributes_ctx, attribute, &isJson);
write_value(writer, attr_name_for_serialization(attribute), value, isJson);
}
}
void init_active_record_attributes_writer(VALUE mPanko) {
attributes_id = rb_intern("@attributes");
delegate_hash_id = rb_intern("@delegate_hash");
values_id = rb_intern("@values");
types_id = rb_intern("@types");
additional_types_id = rb_intern("@additional_types");
type_id = rb_intern("@type");
value_before_type_cast_id = rb_intern("@value_before_type_cast");
fetch_id = rb_intern("fetch");
}
void panko_init_active_record(VALUE mPanko) {
init_active_record_attributes_writer(mPanko);
panko_init_type_cast(mPanko);
}
================================================
FILE: ext/panko_serializer/attributes_writer/active_record.h
================================================
#pragma once
#include <ruby.h>
#include <stdbool.h>
#include "../common.h"
#include "common.h"
#include "serialization_descriptor/attribute.h"
#include "type_cast/type_cast.h"
extern void active_record_attributes_writer(VALUE object, VALUE attributes,
EachAttributeFunc func,
VALUE writer);
void init_active_record_attributes_writer(VALUE mPanko);
void panko_init_active_record(VALUE mPanko);
================================================
FILE: ext/panko_serializer/attributes_writer/attributes_writer.c
================================================
#include "attributes_writer.h"
static bool types_initialized = false;
static VALUE ar_base_type = Qundef;
VALUE init_types(VALUE v) {
if (types_initialized == true) {
return Qundef;
}
types_initialized = true;
volatile VALUE ar_type =
rb_const_get_at(rb_cObject, rb_intern("ActiveRecord"));
ar_base_type = rb_const_get_at(ar_type, rb_intern("Base"));
rb_global_variable(&ar_base_type);
return Qundef;
}
AttributesWriter create_attributes_writer(VALUE object) {
// If ActiveRecord::Base can't be found it will throw error
int isErrored;
rb_protect(init_types, Qnil, &isErrored);
if (ar_base_type != Qundef &&
rb_obj_is_kind_of(object, ar_base_type) == Qtrue) {
return (AttributesWriter){
.object_type = ActiveRecord,
.write_attributes = active_record_attributes_writer};
}
if (!RB_SPECIAL_CONST_P(object) && BUILTIN_TYPE(object) == T_HASH) {
return (AttributesWriter){.object_type = Hash,
.write_attributes = hash_attributes_writer};
}
return (AttributesWriter){.object_type = Plain,
.write_attributes = plain_attributes_writer};
return create_empty_attributes_writer();
}
void empty_write_attributes(VALUE obj, VALUE attributes, EachAttributeFunc func,
VALUE writer) {}
AttributesWriter create_empty_attributes_writer() {
return (AttributesWriter){.object_type = UnknownObjectType,
.write_attributes = empty_write_attributes};
}
void init_attributes_writer(VALUE mPanko) {
init_active_record_attributes_writer(mPanko);
}
================================================
FILE: ext/panko_serializer/attributes_writer/attributes_writer.h
================================================
#pragma once
#include <ruby.h>
#include "active_record.h"
#include "common.h"
#include "hash.h"
#include "plain.h"
enum ObjectType {
UnknownObjectType = 0,
ActiveRecord = 1,
Plain = 2,
Hash = 3
};
typedef struct _AttributesWriter {
enum ObjectType object_type;
void (*write_attributes)(VALUE object, VALUE attributes,
EachAttributeFunc func, VALUE context);
} AttributesWriter;
/**
* Infers the attributes writer from the object type
*/
AttributesWriter create_attributes_writer(VALUE object);
/**
* Creates empty writer
* Useful when the writer is not known, and you need init something
*/
AttributesWriter create_empty_attributes_writer();
void init_attributes_writer(VALUE mPanko);
================================================
FILE: ext/panko_serializer/attributes_writer/common.c
================================================
#include "common.h"
VALUE attr_name_for_serialization(Attribute attribute) {
volatile VALUE name_str = attribute->name_str;
if (attribute->alias_name != Qnil) {
name_str = attribute->alias_name;
}
return name_str;
}
================================================
FILE: ext/panko_serializer/attributes_writer/common.h
================================================
#pragma once
#include "../serialization_descriptor/attribute.h"
#include "ruby.h"
typedef void (*EachAttributeFunc)(VALUE writer, VALUE name, VALUE value,
VALUE isJson);
VALUE attr_name_for_serialization(Attribute attribute);
================================================
FILE: ext/panko_serializer/attributes_writer/hash.c
================================================
#include "hash.h"
void hash_attributes_writer(VALUE obj, VALUE attributes,
EachAttributeFunc write_value, VALUE writer) {
long i;
for (i = 0; i < RARRAY_LEN(attributes); i++) {
volatile VALUE raw_attribute = RARRAY_AREF(attributes, i);
Attribute attribute = attribute_read(raw_attribute);
write_value(writer, attr_name_for_serialization(attribute),
rb_hash_aref(obj, attribute->name_str), Qfalse);
}
}
================================================
FILE: ext/panko_serializer/attributes_writer/hash.h
================================================
#pragma once
#include "common.h"
#include "ruby.h"
void hash_attributes_writer(VALUE obj, VALUE attributes, EachAttributeFunc func,
VALUE writer);
================================================
FILE: ext/panko_serializer/attributes_writer/plain.c
================================================
#include "plain.h"
void plain_attributes_writer(VALUE obj, VALUE attributes,
EachAttributeFunc write_value, VALUE writer) {
long i;
for (i = 0; i < RARRAY_LEN(attributes); i++) {
volatile VALUE raw_attribute = RARRAY_AREF(attributes, i);
Attribute attribute = attribute_read(raw_attribute);
write_value(writer, attr_name_for_serialization(attribute),
rb_funcall(obj, attribute->name_id, 0), Qfalse);
}
}
================================================
FILE: ext/panko_serializer/attributes_writer/plain.h
================================================
#pragma once
#include "common.h"
#include "ruby.h"
void plain_attributes_writer(VALUE obj, VALUE attributes,
EachAttributeFunc func, VALUE writer);
================================================
FILE: ext/panko_serializer/attributes_writer/type_cast/time_conversion.c
================================================
#include "time_conversion.h"
const int YEAR_REGION = 1;
const int MONTH_REGION = 2;
const int DAY_REGION = 3;
const int HOUR_REGION = 4;
const int MINUTE_REGION = 5;
const int SECOND_REGION = 6;
static regex_t* iso8601_time_regex;
static regex_t* ar_iso_datetime_regex;
VALUE is_iso8601_time_string(const char* value) {
const UChar *start, *range, *end;
OnigPosition r;
const UChar* str = (const UChar*)(value);
end = str + strlen(value);
start = str;
range = end;
r = onig_search(iso8601_time_regex, str, end, start, range, NULL,
ONIG_OPTION_NONE);
return r >= 0 ? Qtrue : Qfalse;
}
void append_region_str(const char* source, char** to, int regionBegin,
int regionEnd) {
long iter = 0;
for (iter = regionBegin; iter < regionEnd; iter++) {
*(*to)++ = source[iter];
}
}
bool is_iso_ar_iso_datetime_string_fast_case(const char* value) {
return (
// year
isdigit(value[0]) && isdigit(value[1]) && isdigit(value[2]) &&
isdigit(value[3]) && value[4] == '-' &&
// month
isdigit(value[5]) && isdigit(value[6]) && value[7] == '-' &&
// mday
isdigit(value[8]) && isdigit(value[9]) && value[10] == ' ' &&
// hour
isdigit(value[11]) && isdigit(value[12]) && value[13] == ':' &&
// minute
isdigit(value[14]) && isdigit(value[15]) && value[16] == ':' &&
// seconds
isdigit(value[17]) && isdigit(value[18]));
}
bool is_iso_ar_iso_datetime_string_slow_case(const char* value) {
const UChar *start, *range, *end;
OnigPosition r;
OnigRegion* region = onig_region_new();
const UChar* str = (const UChar*)(value);
end = str + strlen(value);
start = str;
range = end;
r = onig_search(ar_iso_datetime_regex, str, end, start, range, region,
ONIG_OPTION_NONE);
onig_region_free(region, 1);
return (r >= 0);
}
VALUE iso_ar_iso_datetime_string(const char* value) {
if (is_iso_ar_iso_datetime_string_fast_case(value) == true ||
is_iso_ar_iso_datetime_string_slow_case(value) == true) {
volatile VALUE output;
char buf[24] = "";
char* cur = buf;
append_region_str(value, &cur, 0, 4);
*cur++ = '-';
append_region_str(value, &cur, 5, 7);
*cur++ = '-';
append_region_str(value, &cur, 8, 10);
*cur++ = 'T';
append_region_str(value, &cur, 11, 13);
*cur++ = ':';
append_region_str(value, &cur, 14, 16);
*cur++ = ':';
append_region_str(value, &cur, 17, 19);
*cur++ = '.';
if (value[19] == '.' && isdigit(value[20])) {
if (isdigit(value[20])) {
*cur++ = value[20];
} else {
*cur++ = '0';
}
if (isdigit(value[21])) {
*cur++ = value[21];
} else {
*cur++ = '0';
}
if (isdigit(value[22])) {
*cur++ = value[22];
} else {
*cur++ = '0';
}
} else {
*cur++ = '0';
*cur++ = '0';
*cur++ = '0';
}
*cur++ = 'Z';
output = rb_str_new(buf, cur - buf);
return output;
}
return Qnil;
}
void build_regex(OnigRegex* reg, const UChar* pattern) {
OnigErrorInfo einfo;
int r = onig_new(reg, pattern, pattern + strlen((char*)pattern),
ONIG_OPTION_DEFAULT, ONIG_ENCODING_ASCII,
ONIG_SYNTAX_DEFAULT, &einfo);
if (r != ONIG_NORMAL) {
char s[ONIG_MAX_ERROR_MESSAGE_LEN];
onig_error_code_to_str((UChar*)s, r, &einfo);
printf("ERROR: %s\n", s);
}
}
void panko_init_time(VALUE mPanko) {
const UChar *ISO8601_PATTERN, *AR_ISO_DATETIME_PATTERN;
ISO8601_PATTERN =
(UChar*)"^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24\\:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$";
build_regex(&iso8601_time_regex, ISO8601_PATTERN);
AR_ISO_DATETIME_PATTERN =
(UChar*)"\\A(?<year>\\d{4})-(?<month>\\d\\d)-(?<mday>\\d\\d) (?<hour>\\d\\d):(?<min>\\d\\d):(?<sec>\\d\\d)(\\.(?<microsec>\\d+))?\\z";
build_regex(&ar_iso_datetime_regex, AR_ISO_DATETIME_PATTERN);
}
================================================
FILE: ext/panko_serializer/attributes_writer/type_cast/time_conversion.h
================================================
#pragma once
#include <ctype.h>
#include <ruby.h>
#include <ruby/oniguruma.h>
#include <stdbool.h>
VALUE is_iso8601_time_string(const char* value);
VALUE iso_ar_iso_datetime_string(const char* value);
void panko_init_time(VALUE mPanko);
================================================
FILE: ext/panko_serializer/attributes_writer/type_cast/type_cast.c
================================================
#include "type_cast.h"
#include "time_conversion.h"
ID deserialize_from_db_id = 0;
ID to_s_id = 0;
ID to_i_id = 0;
static VALUE oj_type = Qundef;
static VALUE oj_parseerror_type = Qundef;
ID oj_sc_parse_id = 0;
// Caching ActiveRecord Types
static VALUE ar_string_type = Qundef;
static VALUE ar_text_type = Qundef;
static VALUE ar_float_type = Qundef;
static VALUE ar_integer_type = Qundef;
static VALUE ar_boolean_type = Qundef;
static VALUE ar_date_time_type = Qundef;
static VALUE ar_time_zone_converter = Qundef;
static VALUE ar_json_type = Qundef;
static VALUE ar_pg_integer_type = Qundef;
static VALUE ar_pg_float_type = Qundef;
static VALUE ar_pg_uuid_type = Qundef;
static VALUE ar_pg_json_type = Qundef;
static VALUE ar_pg_jsonb_type = Qundef;
static VALUE ar_pg_array_type = Qundef;
static VALUE ar_pg_date_time_type = Qundef;
static VALUE ar_pg_timestamp_type = Qundef;
static int initiailized = 0;
VALUE cache_postgres_type_lookup(VALUE ar) {
VALUE ar_connection_adapters, ar_postgresql, ar_oid;
if (rb_const_defined_at(ar, rb_intern("ConnectionAdapters")) != (int)Qtrue) {
return Qfalse;
}
ar_connection_adapters = rb_const_get_at(ar, rb_intern("ConnectionAdapters"));
if (rb_const_defined_at(ar_connection_adapters, rb_intern("PostgreSQL")) !=
(int)Qtrue) {
return Qfalse;
}
ar_postgresql =
rb_const_get_at(ar_connection_adapters, rb_intern("PostgreSQL"));
if (rb_const_defined_at(ar_postgresql, rb_intern("OID")) != (int)Qtrue) {
return Qfalse;
}
ar_oid = rb_const_get_at(ar_postgresql, rb_intern("OID"));
if (rb_const_defined_at(ar_oid, rb_intern("Float")) == (int)Qtrue) {
ar_pg_float_type = rb_const_get_at(ar_oid, rb_intern("Float"));
}
if (rb_const_defined_at(ar_oid, rb_intern("Integer")) == (int)Qtrue) {
ar_pg_integer_type = rb_const_get_at(ar_oid, rb_intern("Integer"));
}
if (rb_const_defined_at(ar_oid, rb_intern("Uuid")) == (int)Qtrue) {
ar_pg_uuid_type = rb_const_get_at(ar_oid, rb_intern("Uuid"));
}
if (rb_const_defined_at(ar_oid, rb_intern("Json")) == (int)Qtrue) {
ar_pg_json_type = rb_const_get_at(ar_oid, rb_intern("Json"));
}
if (rb_const_defined_at(ar_oid, rb_intern("Jsonb")) == (int)Qtrue) {
ar_pg_jsonb_type = rb_const_get_at(ar_oid, rb_intern("Jsonb"));
}
if (rb_const_defined_at(ar_oid, rb_intern("DateTime")) == (int)Qtrue) {
ar_pg_date_time_type = rb_const_get_at(ar_oid, rb_intern("DateTime"));
}
if (rb_const_defined_at(ar_oid, rb_intern("Timestamp")) == (int)Qtrue) {
ar_pg_timestamp_type = rb_const_get_at(ar_oid, rb_intern("Timestamp"));
}
return Qtrue;
}
VALUE cache_time_zone_type_lookup(VALUE ar) {
VALUE ar_attr_methods, ar_time_zone_conversion;
// ActiveRecord::AttributeMethods
if (rb_const_defined_at(ar, rb_intern("AttributeMethods")) != (int)Qtrue) {
return Qfalse;
}
ar_attr_methods = rb_const_get_at(ar, rb_intern("AttributeMethods"));
// ActiveRecord::AttributeMethods::TimeZoneConversion
if (rb_const_defined_at(ar_attr_methods, rb_intern("TimeZoneConversion")) !=
(int)Qtrue) {
return Qfalse;
}
ar_time_zone_conversion =
rb_const_get_at(ar_attr_methods, rb_intern("TimeZoneConversion"));
if (rb_const_defined_at(ar_time_zone_conversion,
rb_intern("TimeZoneConverter")) != (int)Qtrue) {
return Qfalse;
}
ar_time_zone_converter =
rb_const_get_at(ar_time_zone_conversion, rb_intern("TimeZoneConverter"));
return Qtrue;
}
void cache_type_lookup() {
if (initiailized == 1) {
return;
}
initiailized = 1;
VALUE ar, ar_type, ar_type_methods;
ar = rb_const_get_at(rb_cObject, rb_intern("ActiveRecord"));
// ActiveRecord::Type
ar_type = rb_const_get_at(ar, rb_intern("Type"));
ar_string_type = rb_const_get_at(ar_type, rb_intern("String"));
ar_text_type = rb_const_get_at(ar_type, rb_intern("Text"));
ar_float_type = rb_const_get_at(ar_type, rb_intern("Float"));
ar_integer_type = rb_const_get_at(ar_type, rb_intern("Integer"));
ar_boolean_type = rb_const_get_at(ar_type, rb_intern("Boolean"));
ar_date_time_type = rb_const_get_at(ar_type, rb_intern("DateTime"));
ar_type_methods = rb_class_instance_methods(0, NULL, ar_string_type);
if (rb_ary_includes(ar_type_methods,
rb_to_symbol(rb_str_new_cstr("deserialize")))) {
deserialize_from_db_id = rb_intern("deserialize");
} else {
deserialize_from_db_id = rb_intern("type_cast_from_database");
}
if (rb_const_defined_at(ar_type, rb_intern("Json")) == (int)Qtrue) {
ar_json_type = rb_const_get_at(ar_type, rb_intern("Json"));
}
int isErrored;
rb_protect(cache_postgres_type_lookup, ar, &isErrored);
if (isErrored) {
rb_set_errinfo(Qnil);
}
rb_protect(cache_time_zone_type_lookup, ar, &isErrored);
if (isErrored) {
rb_set_errinfo(Qnil);
}
}
bool is_string_or_text_type(VALUE type_klass) {
return type_klass == ar_string_type || type_klass == ar_text_type ||
(ar_pg_uuid_type != Qundef && type_klass == ar_pg_uuid_type);
}
VALUE cast_string_or_text_type(VALUE value) {
if (RB_TYPE_P(value, T_STRING)) {
return value;
}
if (value == Qtrue) {
return rb_str_new_cstr("t");
}
if (value == Qfalse) {
return rb_str_new_cstr("f");
}
return rb_funcall(value, to_s_id, 0);
}
bool is_float_type(VALUE type_klass) {
return type_klass == ar_float_type ||
(ar_pg_float_type != Qundef && type_klass == ar_pg_float_type);
}
VALUE cast_float_type(VALUE value) {
if (RB_TYPE_P(value, T_FLOAT)) {
return value;
}
if (RB_TYPE_P(value, T_STRING)) {
const char* val = StringValuePtr(value);
return rb_float_new(strtod(val, NULL));
}
return Qundef;
}
bool is_integer_type(VALUE type_klass) {
return type_klass == ar_integer_type ||
(ar_pg_integer_type != Qundef && type_klass == ar_pg_integer_type);
}
VALUE cast_integer_type(VALUE value) {
if (RB_INTEGER_TYPE_P(value)) {
return value;
}
if (RB_TYPE_P(value, T_STRING)) {
const char* val = StringValuePtr(value);
if (strlen(val) == 0) {
return Qnil;
}
return rb_cstr2inum(val, 10);
}
if (RB_FLOAT_TYPE_P(value)) {
// We are calling the `to_i` here, because ruby internal
// `flo_to_i` is not accessible
return rb_funcall(value, to_i_id, 0);
}
if (value == Qtrue) {
return INT2NUM(1);
}
if (value == Qfalse) {
return INT2NUM(0);
}
// At this point, we handled integer, float, string and booleans
// any thing other than this (array, hashes, etc) should result in nil
return Qnil;
}
bool is_json_type(VALUE type_klass) {
return ((ar_pg_json_type != Qundef && type_klass == ar_pg_json_type) ||
(ar_pg_jsonb_type != Qundef && type_klass == ar_pg_jsonb_type) ||
(ar_json_type != Qundef && type_klass == ar_json_type));
}
bool is_boolean_type(VALUE type_klass) { return type_klass == ar_boolean_type; }
VALUE cast_boolean_type(VALUE value) {
if (value == Qtrue || value == Qfalse) {
return value;
}
if (value == Qnil) {
return Qnil;
}
if (RB_TYPE_P(value, T_STRING)) {
if (RSTRING_LEN(value) == 0) {
return Qnil;
}
const char* val = StringValuePtr(value);
bool isFalseValue =
(*val == '0' || (*val == 'f' || *val == 'F') ||
(strcmp(val, "false") == 0 || strcmp(val, "FALSE") == 0) ||
(strcmp(val, "off") == 0 || strcmp(val, "OFF") == 0));
return isFalseValue ? Qfalse : Qtrue;
}
if (RB_INTEGER_TYPE_P(value)) {
return value == INT2NUM(1) ? Qtrue : Qfalse;
}
return Qundef;
}
bool is_date_time_type(VALUE type_klass) {
return (type_klass == ar_date_time_type) ||
(ar_pg_date_time_type != Qundef &&
type_klass == ar_pg_date_time_type) ||
(ar_pg_timestamp_type != Qundef &&
type_klass == ar_pg_timestamp_type) ||
(ar_time_zone_converter != Qundef &&
type_klass == ar_time_zone_converter);
}
VALUE cast_date_time_type(VALUE value) {
// Instead of take strings to comparing them to time zones
// and then comparing them back to string
// We will just make sure we have string on ISO8601 and it's utc
if (RB_TYPE_P(value, T_STRING)) {
const char* val = StringValuePtr(value);
// 'Z' in ISO8601 says it's UTC
if (val[strlen(val) - 1] == 'Z' && is_iso8601_time_string(val) == Qtrue) {
return value;
}
volatile VALUE iso8601_string = iso_ar_iso_datetime_string(val);
if (iso8601_string != Qnil) {
return iso8601_string;
}
}
return Qundef;
}
VALUE rescue_func(VALUE _arg, VALUE _data) { return Qfalse; }
VALUE parse_json(VALUE value) {
return rb_funcall(oj_type, oj_sc_parse_id, 2, rb_cObject, value);
}
VALUE is_json_value(VALUE value) {
if (!RB_TYPE_P(value, T_STRING)) {
return value;
}
if (RSTRING_LEN(value) == 0) {
return Qfalse;
}
volatile VALUE result =
rb_rescue2(parse_json, value, rescue_func, Qundef, oj_parseerror_type, 0);
if (NIL_P(result)) {
return Qtrue;
}
if (result == Qfalse) {
return Qfalse;
}
// TODO: fix me!
return Qfalse;
}
VALUE type_cast(VALUE type_metadata, VALUE value, volatile VALUE* isJson) {
if (value == Qnil || value == Qundef) {
return value;
}
cache_type_lookup();
VALUE type_klass, typeCastedValue;
type_klass = CLASS_OF(type_metadata);
typeCastedValue = Qundef;
TypeCast typeCast;
for (typeCast = type_casts; typeCast->canCast != NULL; typeCast++) {
if (typeCast->canCast(type_klass)) {
typeCastedValue = typeCast->typeCast(value);
break;
}
}
if (is_json_type(type_klass)) {
if (is_json_value(value) == Qfalse) {
return Qnil;
}
*isJson = Qtrue;
return value;
}
if (typeCastedValue == Qundef) {
return rb_funcall(type_metadata, deserialize_from_db_id, 1, value);
}
return typeCastedValue;
}
VALUE public_type_cast(int argc, VALUE* argv, VALUE self) {
VALUE type_metadata, value, isJson;
rb_scan_args(argc, argv, "21", &type_metadata, &value, &isJson);
if (isJson == Qnil || isJson == Qundef) {
isJson = Qfalse;
}
return type_cast(type_metadata, value, &isJson);
}
void panko_init_type_cast(VALUE mPanko) {
to_s_id = rb_intern("to_s");
to_i_id = rb_intern("to_i");
oj_type = rb_const_get_at(rb_cObject, rb_intern("Oj"));
oj_parseerror_type = rb_const_get_at(oj_type, rb_intern("ParseError"));
oj_sc_parse_id = rb_intern("sc_parse");
// TODO: pass 3 arguments here
rb_define_singleton_method(mPanko, "_type_cast", public_type_cast, -1);
panko_init_time(mPanko);
rb_global_variable(&oj_type);
rb_global_variable(&oj_parseerror_type);
rb_global_variable(&ar_string_type);
rb_global_variable(&ar_text_type);
rb_global_variable(&ar_float_type);
rb_global_variable(&ar_integer_type);
rb_global_variable(&ar_boolean_type);
rb_global_variable(&ar_date_time_type);
rb_global_variable(&ar_time_zone_converter);
rb_global_variable(&ar_json_type);
rb_global_variable(&ar_pg_integer_type);
rb_global_variable(&ar_pg_float_type);
rb_global_variable(&ar_pg_uuid_type);
rb_global_variable(&ar_pg_json_type);
rb_global_variable(&ar_pg_jsonb_type);
rb_global_variable(&ar_pg_array_type);
rb_global_variable(&ar_pg_date_time_type);
rb_global_variable(&ar_pg_timestamp_type);
}
================================================
FILE: ext/panko_serializer/attributes_writer/type_cast/type_cast.h
================================================
#pragma once
#include <ruby.h>
#include <stdbool.h>
/*
* Type Casting
*
* We do "special" type casting which is mix of two inspirations:
* *) light records gem
* *) pg TextDecoders
*
* The whole idea behind those type casts, are to do the minimum required
* type casting in the most performant manner and *allocation free*.
*
* For example, in `ActiveRecord::Type::String` the type_cast_from_database
* creates new string, for known reasons, but, in serialization flow we don't
* need to create new string becuase we afraid of mutations.
*
* Since we know before hand, that we are only reading from the database, and
* *not* writing and the end result if for JSON we can skip some "defenses".
*/
typedef bool (*TypeMatchFunc)(VALUE type_klass);
/*
* TypeCastFunc
*
* @return VALUE casted value or Qundef if not casted
*/
typedef VALUE (*TypeCastFunc)(VALUE value);
typedef struct _TypeCast {
TypeMatchFunc canCast;
TypeCastFunc typeCast;
}* TypeCast;
// ActiveRecord::Type::String
// ActiveRecord::Type::Text
bool is_string_or_text_type(VALUE type_klass);
VALUE cast_string_or_text_type(VALUE value);
// ActiveRecord::Type::Float
bool is_float_type(VALUE type_klass);
VALUE cast_float_type(VALUE value);
// ActiveRecord::Type::Integer
bool is_integer_type(VALUE type_klass);
VALUE cast_integer_type(VALUE value);
// ActiveRecord::ConnectoinAdapters::PostgreSQL::Json
bool is_json_type(VALUE type_klass);
VALUE cast_json_type(VALUE value);
// ActiveRecord::Type::Boolean
bool is_boolean_type(VALUE type_klass);
VALUE cast_boolean_type(VALUE value);
// ActiveRecord::Type::DateTime
// ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime
// ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter
bool is_date_time_type(VALUE type_klass);
VALUE cast_date_time_type(VALUE value);
static struct _TypeCast type_casts[] = {
{is_string_or_text_type, cast_string_or_text_type},
{is_integer_type, cast_integer_type},
{is_boolean_type, cast_boolean_type},
{is_date_time_type, cast_date_time_type},
{is_float_type, cast_float_type},
{NULL, NULL}};
extern VALUE type_cast(VALUE type_metadata, VALUE value,
volatile VALUE* isJson);
void panko_init_type_cast(VALUE mPanko);
// Introduced in ruby 2.4
#ifndef RB_INTEGER_TYPE_P
#define RB_INTEGER_TYPE_P(obj) (RB_FIXNUM_P(obj) || RB_TYPE_P(obj, T_BIGNUM))
#endif
================================================
FILE: ext/panko_serializer/common.h
================================================
#pragma once
#include <ruby.h>
#define PANKO_SAFE_HASH_SIZE(hash) \
(hash == Qnil || hash == Qundef) ? 0 : RHASH_SIZE(hash)
#define PANKO_EMPTY_HASH(hash) \
(hash == Qnil || hash == Qundef) ? 1 : (RHASH_SIZE(hash) == 0)
================================================
FILE: ext/panko_serializer/extconf.rb
================================================
# frozen_string_literal: true
require "mkmf"
require "pathname"
$CPPFLAGS += " -Wall"
extension_name = "panko_serializer"
dir_config(extension_name)
RbConfig.expand(srcdir = "$(srcdir)".dup)
# enum all source files
$srcs = Dir[File.join(srcdir, "**/*.c")]
# Get all source directories recursivley
directories = Dir[File.join(srcdir, "**/*")].select { |f| File.directory?(f) }
directories = directories.map { |d| Pathname.new(d).relative_path_from(Pathname.new(srcdir)) }
directories.each do |dir|
# add include path to the internal folder
# $(srcdir) is a root folder, where "extconf.rb" is stored
$INCFLAGS << " -I$(srcdir)/#{dir}"
# add folder, where compiler can search source files
$VPATH << "$(srcdir)/#{dir}"
end
create_makefile("panko/panko_serializer")
================================================
FILE: ext/panko_serializer/panko_serializer.c
================================================
#include "panko_serializer.h"
#include <ruby.h>
static ID push_value_id;
static ID push_array_id;
static ID push_object_id;
static ID push_json_id;
static ID pop_id;
static ID to_a_id;
static ID object_id;
static ID serialization_context_id;
static VALUE SKIP = Qundef;
void write_value(VALUE str_writer, VALUE key, VALUE value, VALUE isJson) {
if (isJson == Qtrue) {
rb_funcall(str_writer, push_json_id, 2, value, key);
} else {
rb_funcall(str_writer, push_value_id, 2, value, key);
}
}
void serialize_method_fields(VALUE object, VALUE str_writer,
SerializationDescriptor descriptor) {
if (RARRAY_LEN(descriptor->method_fields) == 0) {
return;
}
volatile VALUE method_fields, serializer, key;
long i;
method_fields = descriptor->method_fields;
serializer = descriptor->serializer;
rb_ivar_set(serializer, object_id, object);
for (i = 0; i < RARRAY_LEN(method_fields); i++) {
volatile VALUE raw_attribute = RARRAY_AREF(method_fields, i);
Attribute attribute = PANKO_ATTRIBUTE_READ(raw_attribute);
volatile VALUE result = rb_funcall(serializer, attribute->name_id, 0);
if (result != SKIP) {
key = attr_name_for_serialization(attribute);
write_value(str_writer, key, result, Qfalse);
}
}
rb_ivar_set(serializer, object_id, Qnil);
}
void serialize_fields(VALUE object, VALUE str_writer,
SerializationDescriptor descriptor) {
descriptor->attributes_writer.write_attributes(object, descriptor->attributes,
write_value, str_writer);
serialize_method_fields(object, str_writer, descriptor);
}
void serialize_has_one_associations(VALUE object, VALUE str_writer,
VALUE associations) {
long i;
for (i = 0; i < RARRAY_LEN(associations); i++) {
volatile VALUE association_el = RARRAY_AREF(associations, i);
Association association = association_read(association_el);
volatile VALUE value = rb_funcall(object, association->name_id, 0);
if (NIL_P(value)) {
write_value(str_writer, association->name_str, value, Qfalse);
} else {
serialize_object(association->name_str, value, str_writer,
association->descriptor);
}
}
}
void serialize_has_many_associations(VALUE object, VALUE str_writer,
VALUE associations) {
long i;
for (i = 0; i < RARRAY_LEN(associations); i++) {
volatile VALUE association_el = RARRAY_AREF(associations, i);
Association association = association_read(association_el);
volatile VALUE value = rb_funcall(object, association->name_id, 0);
if (NIL_P(value)) {
write_value(str_writer, association->name_str, value, Qfalse);
} else {
serialize_objects(association->name_str, value, str_writer,
association->descriptor);
}
}
}
VALUE serialize_object(VALUE key, VALUE object, VALUE str_writer,
SerializationDescriptor descriptor) {
sd_set_writer(descriptor, object);
rb_funcall(str_writer, push_object_id, 1, key);
serialize_fields(object, str_writer, descriptor);
if (RARRAY_LEN(descriptor->has_one_associations) > 0) {
serialize_has_one_associations(object, str_writer,
descriptor->has_one_associations);
}
if (RARRAY_LEN(descriptor->has_many_associations) > 0) {
serialize_has_many_associations(object, str_writer,
descriptor->has_many_associations);
}
rb_funcall(str_writer, pop_id, 0);
return Qnil;
}
VALUE serialize_objects(VALUE key, VALUE objects, VALUE str_writer,
SerializationDescriptor descriptor) {
long i;
rb_funcall(str_writer, push_array_id, 1, key);
if (!RB_TYPE_P(objects, T_ARRAY)) {
objects = rb_funcall(objects, to_a_id, 0);
}
for (i = 0; i < RARRAY_LEN(objects); i++) {
volatile VALUE object = RARRAY_AREF(objects, i);
serialize_object(Qnil, object, str_writer, descriptor);
}
rb_funcall(str_writer, pop_id, 0);
return Qnil;
}
VALUE serialize_object_api(VALUE klass, VALUE object, VALUE str_writer,
VALUE descriptor) {
SerializationDescriptor sd = sd_read(descriptor);
return serialize_object(Qnil, object, str_writer, sd);
}
VALUE serialize_objects_api(VALUE klass, VALUE objects, VALUE str_writer,
VALUE descriptor) {
serialize_objects(Qnil, objects, str_writer, sd_read(descriptor));
return Qnil;
}
void Init_panko_serializer() {
push_value_id = rb_intern("push_value");
push_array_id = rb_intern("push_array");
push_object_id = rb_intern("push_object");
push_json_id = rb_intern("push_json");
pop_id = rb_intern("pop");
to_a_id = rb_intern("to_a");
object_id = rb_intern("@object");
serialization_context_id = rb_intern("@serialization_context");
VALUE mPanko = rb_define_module("Panko");
rb_define_singleton_method(mPanko, "serialize_object", serialize_object_api,
3);
rb_define_singleton_method(mPanko, "serialize_objects", serialize_objects_api,
3);
VALUE mPankoSerializer = rb_const_get(mPanko, rb_intern("Serializer"));
SKIP = rb_const_get(mPankoSerializer, rb_intern("SKIP"));
rb_global_variable(&SKIP);
panko_init_serialization_descriptor(mPanko);
init_attributes_writer(mPanko);
panko_init_type_cast(mPanko);
panko_init_attribute(mPanko);
panko_init_association(mPanko);
}
================================================
FILE: ext/panko_serializer/panko_serializer.h
================================================
#include <ruby.h>
#include "attributes_writer/attributes_writer.h"
#include "serialization_descriptor/association.h"
#include "serialization_descriptor/attribute.h"
#include "serialization_descriptor/serialization_descriptor.h"
VALUE serialize_object(VALUE key, VALUE object, VALUE str_writer,
SerializationDescriptor descriptor);
VALUE serialize_objects(VALUE key, VALUE objects, VALUE str_writer,
SerializationDescriptor descriptor);
================================================
FILE: ext/panko_serializer/serialization_descriptor/association.c
================================================
#include "association.h"
VALUE cAssociation;
static void association_free(void* ptr) {
if (!ptr) {
return;
}
Association association = (Association)ptr;
association->name_str = Qnil;
association->name_id = 0;
association->name_sym = Qnil;
association->rb_descriptor = Qnil;
if (!association->descriptor || association->descriptor != NULL) {
association->descriptor = NULL;
}
xfree(association);
}
void association_mark(Association data) {
rb_gc_mark(data->name_str);
rb_gc_mark(data->name_sym);
rb_gc_mark(data->rb_descriptor);
if (data->descriptor != NULL) {
sd_mark(data->descriptor);
}
}
static VALUE association_new(int argc, VALUE* argv, VALUE self) {
Association association;
Check_Type(argv[0], T_SYMBOL);
Check_Type(argv[1], T_STRING);
association = ALLOC(struct _Association);
association->name_sym = argv[0];
association->name_str = argv[1];
association->rb_descriptor = argv[2];
association->name_id = rb_intern_str(rb_sym2str(association->name_sym));
association->descriptor = sd_read(association->rb_descriptor);
return Data_Wrap_Struct(cAssociation, association_mark, association_free,
association);
}
Association association_read(VALUE association) {
return (Association)DATA_PTR(association);
}
VALUE association_name_sym_ref(VALUE self) {
Association association = (Association)DATA_PTR(self);
return association->name_sym;
}
VALUE association_name_str_ref(VALUE self) {
Association association = (Association)DATA_PTR(self);
return association->name_str;
}
VALUE association_descriptor_ref(VALUE self) {
Association association = (Association)DATA_PTR(self);
return association->rb_descriptor;
}
VALUE association_decriptor_aset(VALUE self, VALUE descriptor) {
Association association = (Association)DATA_PTR(self);
association->rb_descriptor = descriptor;
association->descriptor = sd_read(descriptor);
return association->rb_descriptor;
}
void panko_init_association(VALUE mPanko) {
cAssociation = rb_define_class_under(mPanko, "Association", rb_cObject);
rb_undef_alloc_func(cAssociation);
rb_global_variable(&cAssociation);
rb_define_module_function(cAssociation, "new", association_new, -1);
rb_define_method(cAssociation, "name_sym", association_name_sym_ref, 0);
rb_define_method(cAssociation, "name_str", association_name_str_ref, 0);
rb_define_method(cAssociation, "descriptor", association_descriptor_ref, 0);
rb_define_method(cAssociation, "descriptor=", association_decriptor_aset, 1);
}
================================================
FILE: ext/panko_serializer/serialization_descriptor/association.h
================================================
#include <ruby.h>
#ifndef __ASSOCIATION_H__
#define __ASSOCIATION_H__
#include "serialization_descriptor.h"
typedef struct _Association {
ID name_id;
VALUE name_sym;
VALUE name_str;
VALUE rb_descriptor;
SerializationDescriptor descriptor;
}* Association;
Association association_read(VALUE association);
void panko_init_association(VALUE mPanko);
#endif
================================================
FILE: ext/panko_serializer/serialization_descriptor/attribute.c
================================================
#include "attribute.h"
ID attribute_aliases_id = 0;
VALUE cAttribute;
static void attribute_free(void* ptr) {
if (!ptr) {
return;
}
Attribute attribute = (Attribute)ptr;
attribute->name_str = Qnil;
attribute->name_id = 0;
attribute->alias_name = Qnil;
attribute->type = Qnil;
attribute->record_class = Qnil;
xfree(attribute);
}
void attribute_mark(Attribute data) {
rb_gc_mark(data->name_str);
rb_gc_mark(data->alias_name);
rb_gc_mark(data->type);
rb_gc_mark(data->record_class);
}
static VALUE attribute_new(int argc, VALUE* argv, VALUE self) {
Attribute attribute;
Check_Type(argv[0], T_STRING);
if (argv[1] != Qnil) {
Check_Type(argv[1], T_STRING);
}
attribute = ALLOC(struct _Attribute);
attribute->name_str = argv[0];
attribute->name_id = rb_intern_str(attribute->name_str);
attribute->alias_name = argv[1];
attribute->type = Qnil;
attribute->record_class = Qnil;
return Data_Wrap_Struct(cAttribute, attribute_mark, attribute_free,
attribute);
}
Attribute attribute_read(VALUE attribute) {
return (Attribute)DATA_PTR(attribute);
}
void attribute_try_invalidate(Attribute attribute, VALUE new_record_class) {
if (attribute->record_class != new_record_class) {
attribute->type = Qnil;
attribute->record_class = new_record_class;
// Once the record class is changed for this attribute, check if
// we attribute_aliases (from ActivRecord), if so fill in
// performance wise - this code should be called once (unless the serialzier
// is polymorphic)
volatile VALUE ar_aliases_hash =
rb_funcall(new_record_class, attribute_aliases_id, 0);
if (!PANKO_EMPTY_HASH(ar_aliases_hash)) {
volatile VALUE aliasedValue =
rb_hash_aref(ar_aliases_hash, attribute->name_str);
if (aliasedValue != Qnil) {
attribute->alias_name = attribute->name_str;
attribute->name_str = aliasedValue;
attribute->name_id = rb_intern_str(attribute->name_str);
}
}
}
}
VALUE attribute_name_ref(VALUE self) {
Attribute attribute = (Attribute)DATA_PTR(self);
return attribute->name_str;
}
VALUE attribute_alias_name_ref(VALUE self) {
Attribute attribute = (Attribute)DATA_PTR(self);
return attribute->alias_name;
}
void panko_init_attribute(VALUE mPanko) {
attribute_aliases_id = rb_intern("attribute_aliases");
cAttribute = rb_define_class_under(mPanko, "Attribute", rb_cObject);
rb_undef_alloc_func(cAttribute);
rb_global_variable(&cAttribute);
rb_define_module_function(cAttribute, "new", attribute_new, -1);
rb_define_method(cAttribute, "name", attribute_name_ref, 0);
rb_define_method(cAttribute, "alias_name", attribute_alias_name_ref, 0);
}
================================================
FILE: ext/panko_serializer/serialization_descriptor/attribute.h
================================================
#include <ruby.h>
#ifndef __ATTRIBUTE_H__
#define __ATTRIBUTE_H__
#include "../common.h"
typedef struct _Attribute {
VALUE name_str;
ID name_id;
VALUE alias_name;
/*
* We will cache the activerecord type
* by the record_class
*/
VALUE type;
VALUE record_class;
}* Attribute;
Attribute attribute_read(VALUE attribute);
void attribute_try_invalidate(Attribute attribute, VALUE new_record_class);
void panko_init_attribute(VALUE mPanko);
#define PANKO_ATTRIBUTE_READ(attribute) (Attribute) DATA_PTR(attribute)
#endif
================================================
FILE: ext/panko_serializer/serialization_descriptor/serialization_descriptor.c
================================================
#include "serialization_descriptor.h"
static ID object_id;
static ID sc_id;
static void sd_free(SerializationDescriptor sd) {
if (!sd) {
return;
}
sd->serializer = Qnil;
sd->serializer_type = Qnil;
sd->attributes = Qnil;
sd->method_fields = Qnil;
sd->has_one_associations = Qnil;
sd->has_many_associations = Qnil;
sd->aliases = Qnil;
xfree(sd);
}
void sd_mark(SerializationDescriptor data) {
rb_gc_mark(data->serializer);
rb_gc_mark(data->serializer_type);
rb_gc_mark(data->attributes);
rb_gc_mark(data->method_fields);
rb_gc_mark(data->has_one_associations);
rb_gc_mark(data->has_many_associations);
rb_gc_mark(data->aliases);
}
static VALUE sd_alloc(VALUE klass) {
SerializationDescriptor sd = ALLOC(struct _SerializationDescriptor);
sd->serializer = Qnil;
sd->serializer_type = Qnil;
sd->attributes = Qnil;
sd->method_fields = Qnil;
sd->has_one_associations = Qnil;
sd->has_many_associations = Qnil;
sd->aliases = Qnil;
sd->attributes_writer = create_empty_attributes_writer();
return Data_Wrap_Struct(klass, sd_mark, sd_free, sd);
}
SerializationDescriptor sd_read(VALUE descriptor) {
return (SerializationDescriptor)DATA_PTR(descriptor);
}
void sd_set_writer(SerializationDescriptor sd, VALUE object) {
if (sd->attributes_writer.object_type != UnknownObjectType) {
return;
}
sd->attributes_writer = create_attributes_writer(object);
}
VALUE sd_serializer_set(VALUE self, VALUE serializer) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
sd->serializer = serializer;
return Qnil;
}
VALUE sd_serializer_ref(VALUE self) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
return sd->serializer;
}
VALUE sd_attributes_set(VALUE self, VALUE attributes) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
sd->attributes = attributes;
return Qnil;
}
VALUE sd_attributes_ref(VALUE self) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
return sd->attributes;
}
VALUE sd_method_fields_set(VALUE self, VALUE method_fields) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
sd->method_fields = method_fields;
return Qnil;
}
VALUE sd_method_fields_ref(VALUE self) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
return sd->method_fields;
}
VALUE sd_has_one_associations_set(VALUE self, VALUE has_one_associations) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
sd->has_one_associations = has_one_associations;
return Qnil;
}
VALUE sd_has_one_associations_ref(VALUE self) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
return sd->has_one_associations;
}
VALUE sd_has_many_associations_set(VALUE self, VALUE has_many_associations) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
sd->has_many_associations = has_many_associations;
return Qnil;
}
VALUE sd_has_many_associations_ref(VALUE self) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
return sd->has_many_associations;
}
VALUE sd_type_set(VALUE self, VALUE type) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
sd->serializer_type = type;
return Qnil;
}
VALUE sd_type_aref(VALUE self) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
return sd->serializer_type;
}
VALUE sd_aliases_set(VALUE self, VALUE aliases) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
sd->aliases = aliases;
return Qnil;
}
VALUE sd_aliases_aref(VALUE self) {
SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
return sd->aliases;
}
void panko_init_serialization_descriptor(VALUE mPanko) {
object_id = rb_intern("@object");
sc_id = rb_intern("@sc");
VALUE cSerializationDescriptor =
rb_define_class_under(mPanko, "SerializationDescriptor", rb_cObject);
rb_define_alloc_func(cSerializationDescriptor, sd_alloc);
rb_define_method(cSerializationDescriptor, "serializer=", sd_serializer_set,
1);
rb_define_method(cSerializationDescriptor, "serializer", sd_serializer_ref,
0);
rb_define_method(cSerializationDescriptor, "attributes=", sd_attributes_set,
1);
rb_define_method(cSerializationDescriptor, "attributes", sd_attributes_ref,
0);
rb_define_method(cSerializationDescriptor,
"method_fields=", sd_method_fields_set, 1);
rb_define_method(cSerializationDescriptor, "method_fields",
sd_method_fields_ref, 0);
rb_define_method(cSerializationDescriptor,
"has_one_associations=", sd_has_one_associations_set, 1);
rb_define_method(cSerializationDescriptor, "has_one_associations",
sd_has_one_associations_ref, 0);
rb_define_method(cSerializationDescriptor,
"has_many_associations=", sd_has_many_associations_set, 1);
rb_define_method(cSerializationDescriptor, "has_many_associations",
sd_has_many_associations_ref, 0);
rb_define_method(cSerializationDescriptor, "type=", sd_type_set, 1);
rb_define_method(cSerializationDescriptor, "type", sd_type_aref, 0);
rb_define_method(cSerializationDescriptor, "aliases=", sd_aliases_set, 1);
rb_define_method(cSerializationDescriptor, "aliases", sd_aliases_aref, 0);
}
================================================
FILE: ext/panko_serializer/serialization_descriptor/serialization_descriptor.h
================================================
#pragma once
#include <ruby.h>
#include <stdbool.h>
#include "attributes_writer/attributes_writer.h"
typedef struct _SerializationDescriptor {
// type of the serializer, so we can create it later
VALUE serializer_type;
// Cached value of the serializer
VALUE serializer;
// Metadata
VALUE attributes;
VALUE aliases;
VALUE method_fields;
VALUE has_one_associations;
VALUE has_many_associations;
AttributesWriter attributes_writer;
}* SerializationDescriptor;
SerializationDescriptor sd_read(VALUE descriptor);
void sd_mark(SerializationDescriptor data);
void sd_set_writer(SerializationDescriptor sd, VALUE object);
void panko_init_serialization_descriptor(VALUE mPanko);
================================================
FILE: gemfiles/7.2.0.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "activesupport", "~> 7.2.0"
gem "activemodel", "~> 7.2.0"
gem "activerecord", "~> 7.2.0", group: :test
gem "trilogy"
gem "sqlite3", "~> 1.4"
group :benchmarks do
gem "vernier"
gem "stackprof"
gem "pg"
gem "benchmark-ips"
gem "memory_profiler"
end
group :test do
gem "faker"
gem "temping"
end
group :development do
gem "byebug"
gem "rake"
gem "rspec", "~> 3.0"
gem "rake-compiler"
end
group :development, :test do
gem "rubocop"
gem "standard"
gem "standard-performance"
gem "rubocop-performance"
gem "rubocop-rspec"
end
gemspec path: "../"
================================================
FILE: gemfiles/8.0.0.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "activesupport", "~> 8.0.0"
gem "activemodel", "~> 8.0.0"
gem "activerecord", "~> 8.0.0", group: :test
gem "trilogy"
gem "sqlite3", ">= 2.1"
group :benchmarks do
gem "vernier"
gem "stackprof"
gem "pg"
gem "benchmark-ips"
gem "memory_profiler"
end
group :test do
gem "faker"
gem "temping"
end
group :development do
gem "byebug"
gem "rake"
gem "rspec", "~> 3.0"
gem "rake-compiler"
end
group :development, :test do
gem "rubocop"
gem "standard"
gem "standard-performance"
gem "rubocop-performance"
gem "rubocop-rspec"
end
gemspec path: "../"
================================================
FILE: gemfiles/8.1.0.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "activesupport", "~> 8.1.0"
gem "activemodel", "~> 8.1.0"
gem "activerecord", "~> 8.1.0", group: :test
gem "trilogy"
gem "sqlite3", ">= 2.1"
group :benchmarks do
gem "vernier"
gem "stackprof"
gem "pg"
gem "benchmark-ips"
gem "memory_profiler"
end
group :test do
gem "faker"
gem "temping"
end
group :development do
gem "byebug"
gem "rake"
gem "rspec", "~> 3.0"
gem "rake-compiler"
end
group :development, :test do
gem "rubocop"
gem "standard"
gem "standard-performance"
gem "rubocop-performance"
gem "rubocop-rspec"
end
gemspec path: "../"
================================================
FILE: lib/panko/array_serializer.rb
================================================
# frozen_string_literal: true
module Panko
class ArraySerializer
attr_accessor :subjects
def initialize(subjects, options = {})
@subjects = subjects
@each_serializer = options[:each_serializer]
if @each_serializer.nil?
raise ArgumentError, %{
Please pass valid each_serializer to ArraySerializer, for example:
> Panko::ArraySerializer.new(posts, each_serializer: PostSerializer)
}
end
serializer_options = {
only: options.fetch(:only, []),
except: options.fetch(:except, []),
context: options[:context],
scope: options[:scope]
}
@serialization_context = SerializationContext.create(options)
@descriptor = Panko::SerializationDescriptor.build(@each_serializer, serializer_options, @serialization_context)
end
def to_json
serialize_to_json @subjects
end
def serialize(subjects)
serialize_with_writer(subjects, Panko::ObjectWriter.new).output
end
def to_a
serialize_with_writer(@subjects, Panko::ObjectWriter.new).output
end
def serialize_to_json(subjects)
serialize_with_writer(subjects, Oj::StringWriter.new(mode: :rails)).to_s
end
private
def serialize_with_writer(subjects, writer)
Panko.serialize_objects(subjects.to_a, writer, @descriptor)
writer
end
end
end
================================================
FILE: lib/panko/association.rb
================================================
# frozen_string_literal: true
module Panko
class Association
def duplicate
Panko::Association.new(
name_sym,
name_str,
Panko::SerializationDescriptor.duplicate(descriptor)
)
end
def inspect
"<Panko::Association name=#{name_str.inspect}>"
end
end
end
================================================
FILE: lib/panko/attribute.rb
================================================
# frozen_string_literal: true
module Panko
class Attribute
def self.create(name, alias_name: nil)
alias_name = alias_name.to_s unless alias_name.nil?
Attribute.new(name.to_s, alias_name)
end
def ==(other)
return name.to_sym == other if other.is_a? Symbol
return name == other.name && alias_name == other.alias_name if other.is_a? Panko::Attribute
super
end
def hash
name.to_sym.hash
end
def eql?(other)
self.==(other)
end
def inspect
"<Panko::Attribute name=#{name.inspect} alias_name=#{alias_name.inspect}>"
end
end
end
================================================
FILE: lib/panko/object_writer.rb
================================================
# frozen_string_literal: true
class Panko::ObjectWriter
def initialize
@values = []
@keys = []
@next_key = nil
@output = nil
end
def push_object(key = nil)
@values << {}
@keys << key
end
def push_array(key = nil)
@values << []
@keys << key
end
def push_key(key)
@next_key = key
end
def push_value(value, key = nil)
unless @next_key.nil?
raise "push_value is called with key after push_key is called" unless key.nil?
key = @next_key
@next_key = nil
end
@values.last[key] = value.as_json
end
def push_json(value, key = nil)
if value.is_a?(String)
value = begin
Oj.load(value)
rescue
nil
end
end
push_value(value, key)
end
def pop
result = @values.pop
if @values.empty?
@output = result
return
end
scope_key = @keys.pop
if scope_key.nil?
@values.last << result
else
@values.last[scope_key] = result
end
end
def output
raise "Output is called before poping all" unless @values.empty?
@output
end
end
================================================
FILE: lib/panko/response.rb
================================================
# frozen_string_literal: true
require "oj"
module Panko
JsonValue = Struct.new(:value) do
def self.from(value)
JsonValue.new(value)
end
def to_json
value
end
end
class ResponseCreator
def self.value(value)
Panko::Response.new(value)
end
def self.json(value)
Panko::JsonValue.from(value)
end
def self.array_serializer(data, serializer, options = {})
merged_options = options.merge(each_serializer: serializer)
Panko::ArraySerializer.new(data, merged_options)
end
def self.serializer(data, serializer, options = {})
json serializer.new(options).serialize_to_json(data)
end
end
class Response
def initialize(data)
@data = data
end
def to_json(_options = nil)
writer = Oj::StringWriter.new(mode: :rails)
write(writer, @data)
writer.to_s
end
def self.create
Response.new(yield ResponseCreator)
end
private
def write(writer, data, key = nil)
return write_array(writer, data, key) if data.is_a?(Array)
return write_object(writer, data, key) if data.is_a?(Hash)
write_value(writer, data, key)
end
def write_array(writer, value, key = nil)
writer.push_array key
value.each { |v| write(writer, v) }
writer.pop
end
def write_object(writer, value, key = nil)
writer.push_object key
value.each do |entry_key, entry_value|
write(writer, entry_value, entry_key.to_s)
end
writer.pop
end
def write_value(writer, value, key = nil)
if value.is_a?(Panko::ArraySerializer) ||
value.is_a?(Panko::Serializer) ||
value.is_a?(Panko::Response) ||
value.is_a?(Panko::JsonValue)
writer.push_json(value.to_json, key)
else
writer.push_value(value, key)
end
end
end
end
================================================
FILE: lib/panko/serialization_descriptor.rb
================================================
# frozen_string_literal: true
module Panko
class SerializationDescriptor
#
# Creates new description and apply the options
# on the new descriptor
#
def self.build(serializer, options = {}, serialization_context = nil)
backend = Panko::SerializationDescriptor.duplicate(serializer._descriptor)
options.merge! serializer.filters_for(options[:context], options[:scope]) if serializer.respond_to? :filters_for
backend.apply_filters(options)
backend.set_serialization_context(serialization_context)
backend
end
#
# Create new descriptor with same properties
# useful when you want to apply filters
#
def self.duplicate(descriptor)
backend = Panko::SerializationDescriptor.new
backend.type = descriptor.type
backend.attributes = descriptor.attributes.dup
backend.method_fields = descriptor.method_fields.dup
backend.serializer = descriptor.type.new(_skip_init: true) unless backend.method_fields.empty?
backend.has_many_associations = descriptor.has_many_associations.map(&:duplicate)
backend.has_one_associations = descriptor.has_one_associations.map(&:duplicate)
backend
end
def set_serialization_context(context)
serializer.serialization_context = context if !method_fields.empty? && !serializer.nil?
has_many_associations.each do |assoc|
assoc.descriptor.set_serialization_context context
end
has_one_associations.each do |assoc|
assoc.descriptor.set_serialization_context context
end
end
#
# Applies attributes and association filters
#
def apply_filters(options)
return unless options.key?(:only) || options.key?(:except)
attributes_only_filters, associations_only_filters = resolve_filters(options, :only)
attributes_except_filters, associations_except_filters = resolve_filters(options, :except)
self.attributes = apply_attribute_filters(
attributes,
attributes_only_filters,
attributes_except_filters
)
self.method_fields = apply_attribute_filters(
method_fields,
attributes_only_filters,
attributes_except_filters
)
unless has_many_associations.empty?
self.has_many_associations = apply_association_filters(
has_many_associations,
{attributes: attributes_only_filters, associations: associations_only_filters},
attributes: attributes_except_filters, associations: associations_except_filters
)
end
unless has_one_associations.empty?
self.has_one_associations = apply_association_filters(
has_one_associations,
{attributes: attributes_only_filters, associations: associations_only_filters},
attributes: attributes_except_filters, associations: associations_except_filters
)
end
end
def apply_association_filters(associations, only_filters, except_filters)
attributes_only_filters = only_filters[:attributes] || []
unless attributes_only_filters.empty?
associations.select! do |association|
attributes_only_filters.include?(association.name_sym)
end
end
attributes_except_filters = except_filters[:attributes] || []
unless attributes_except_filters.empty?
associations.reject! do |association|
attributes_except_filters.include?(association.name_sym)
end
end
associations_only_filters = only_filters[:associations]
associations_except_filters = except_filters[:associations]
return associations if associations_only_filters.empty? && associations_except_filters.empty?
associations.map do |association|
name = association.name_sym
descriptor = association.descriptor
only_filter = associations_only_filters[name]
except_filter = associations_except_filters[name]
filters = {}
filters[:only] = only_filter unless only_filter.nil?
filters[:except] = except_filter unless except_filter.nil?
unless filters.empty?
next Panko::Association.new(
name,
association.name_str,
Panko::SerializationDescriptor.build(descriptor.type, filters)
)
end
association
end
end
def resolve_filters(options, filter)
filters = options.fetch(filter, {})
return filters, {} if filters.is_a? Array
# hash filters looks like this
# { instance: [:a], foo: [:b] }
# which mean, for the current instance use `[:a]` as filter
# and for association named `foo` use `[:b]`
return [], {} if filters.empty?
attributes_filters = filters.fetch(:instance, [])
association_filters = filters.except(:instance)
[attributes_filters, association_filters]
end
def apply_fields_filters(fields, only, except)
return fields & only unless only.empty?
return fields - except unless except.empty?
fields
end
def apply_attribute_filters(attributes, only, except)
unless only.empty?
attributes = attributes.select do |attribute|
name_to_check = attribute.name
name_to_check = attribute.alias_name unless attribute.alias_name.nil?
only.include?(name_to_check.to_sym)
end
end
unless except.empty?
attributes = attributes.reject do |attribute|
name_to_check = attribute.name
name_to_check = attribute.alias_name unless attribute.alias_name.nil?
except.include?(name_to_check.to_sym)
end
end
attributes
end
end
end
================================================
FILE: lib/panko/serializer.rb
================================================
# frozen_string_literal: true
require_relative "serialization_descriptor"
require "oj"
class SerializationContext
attr_accessor :context, :scope
def initialize(context, scope)
@context = context
@scope = scope
end
def self.create(options)
if options.key?(:context) || options.key?(:scope)
SerializationContext.new(options[:context], options[:scope])
else
EmptySerializerContext.new
end
end
end
class EmptySerializerContext
def scope
nil
end
def context
nil
end
end
module Panko
class Serializer
SKIP = Object.new.freeze
class << self
def inherited(base)
if _descriptor.nil?
base._descriptor = Panko::SerializationDescriptor.new
base._descriptor.attributes = []
base._descriptor.aliases = {}
base._descriptor.method_fields = []
base._descriptor.has_many_associations = []
base._descriptor.has_one_associations = []
else
base._descriptor = Panko::SerializationDescriptor.duplicate(_descriptor)
end
base._descriptor.type = base
end
attr_accessor :_descriptor
def attributes(*attrs)
@_descriptor.attributes.push(*attrs.map { |attr| Attribute.create(attr) }).uniq!
end
def aliases(aliases = {})
aliases.each do |attr, alias_name|
@_descriptor.attributes << Attribute.create(attr, alias_name: alias_name)
end
end
def method_added(method)
super
return if @_descriptor.nil?
deleted_attr = @_descriptor.attributes.delete(method)
@_descriptor.method_fields << Attribute.create(deleted_attr.name, alias_name: deleted_attr.alias_name) unless deleted_attr.nil?
end
def has_one(name, options = {})
serializer_const = options[:serializer]
if serializer_const.is_a?(String)
serializer_const = Panko::SerializerResolver.resolve(serializer_const, self)
end
serializer_const ||= Panko::SerializerResolver.resolve(name.to_s, self)
raise "Can't find serializer for #{self.name}.#{name} has_one relationship." if serializer_const.nil?
@_descriptor.has_one_associations << Panko::Association.new(
name,
options.fetch(:name, name).to_s,
Panko::SerializationDescriptor.build(serializer_const, options)
)
end
def has_many(name, options = {})
serializer_const = options[:serializer] || options[:each_serializer]
if serializer_const.is_a?(String)
serializer_const = Panko::SerializerResolver.resolve(serializer_const, self)
end
serializer_const ||= Panko::SerializerResolver.resolve(name.to_s, self)
raise "Can't find serializer for #{self.name}.#{name} has_many relationship." if serializer_const.nil?
@_descriptor.has_many_associations << Panko::Association.new(
name,
options.fetch(:name, name).to_s,
Panko::SerializationDescriptor.build(serializer_const, options)
)
end
end
def initialize(options = {})
# this "_skip_init" trick is so I can create serializers from serialization descriptor
return if options[:_skip_init]
@serialization_context = SerializationContext.create(options)
@descriptor = Panko::SerializationDescriptor.build(self.class, options, @serialization_context)
@used = false
end
def context
@serialization_context.context
end
def scope
@serialization_context.scope
end
attr_writer :serialization_context
attr_reader :object
def serialize(object)
serialize_with_writer(object, Panko::ObjectWriter.new).output
end
def serialize_to_json(object)
serialize_with_writer(object, Oj::StringWriter.new(mode: :rails)).to_s
end
private
def serialize_with_writer(object, writer)
raise ArgumentError.new("Panko::Serializer instances are single-use") if @used
Panko.serialize_object(object, writer, @descriptor)
@used = true
writer
end
end
end
================================================
FILE: lib/panko/serializer_resolver.rb
================================================
# frozen_string_literal: true
require "active_support/core_ext/string/inflections"
require "active_support/core_ext/module/introspection"
class Panko::SerializerResolver
class << self
def resolve(name, from)
serializer_const = nil
namespace = namespace_for(from)
if namespace.present?
serializer_const = safe_serializer_get("#{namespace}::#{name.singularize.camelize}Serializer")
end
serializer_const ||= safe_serializer_get("#{name.singularize.camelize}Serializer")
serializer_const ||= safe_serializer_get(name)
serializer_const
end
private
if Module.method_defined?(:module_parent_name)
def namespace_for(from)
from.module_parent_name
end
else
def namespace_for(from)
from.parent_name
end
end
def safe_serializer_get(name)
const = Object.const_get(name)
(const < Panko::Serializer) ? const : nil
rescue NameError
nil
end
end
end
================================================
FILE: lib/panko/version.rb
================================================
# frozen_string_literal: true
module Panko
VERSION = "0.8.5"
end
================================================
FILE: lib/panko_serializer.rb
================================================
# frozen_string_literal: true
require "panko/version"
require "panko/attribute"
require "panko/association"
require "panko/serializer"
require "panko/array_serializer"
require "panko/response"
require "panko/serializer_resolver"
require "panko/object_writer"
# C Extension
require "oj"
require "panko/panko_serializer"
================================================
FILE: panko_serializer.gemspec
================================================
# frozen_string_literal: true
lib = File.expand_path("lib", __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "panko/version"
Gem::Specification.new do |spec|
spec.name = "panko_serializer"
spec.version = Panko::VERSION
spec.authors = ["Yosi Attias"]
spec.email = ["yosy101@gmail.com"]
spec.summary = "High Performance JSON Serialization for ActiveRecord & Ruby Objects"
spec.homepage = "https://panko.dev"
spec.license = "MIT"
spec.metadata = {
"bug_tracker_uri" => "https://github.com/yosiat/panko_serializer/issues",
"source_code_uri" => "https://github.com/yosiat/panko_serializer",
"documentation_uri" => "https://panko.dev",
"changelog_uri" => "https://github.com/yosiat/panko_serializer/releases"
}
spec.required_ruby_version = ">= 3.1.0"
spec.files = Dir["{ext,lib}/**/*", "LICENSE.txt", "README.md"] & `git ls-files -z`.split("\x0")
spec.require_paths = ["lib"]
spec.extensions << "ext/panko_serializer/extconf.rb"
spec.add_dependency "oj", "> 3.11.0", "< 4.0.0"
spec.add_dependency "activesupport"
spec.add_development_dependency "appraisal"
end
================================================
FILE: spec/features/active_record_serialization_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"
describe "ActiveRecord Serialization" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
it "serializes objects from database" do
class FooSerializer < Panko::Serializer
attributes :name, :address
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word).reload
expect(foo).to serialized_as(FooSerializer,
"name" => foo.name,
"address" => foo.address)
end
it "serializes objects from memory" do
class FooSerializer < Panko::Serializer
attributes :name, :address
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(foo).to serialized_as(FooSerializer,
"name" => foo.name,
"address" => foo.address)
end
it "preserves changed attributes" do
class FooSerializer < Panko::Serializer
attributes :name, :address
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word).reload
foo.update!(name: "This is a new name")
expect(foo).to serialized_as(FooSerializer,
"name" => "This is a new name",
"address" => foo.address)
end
end
================================================
FILE: spec/features/array_serializer_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"
describe Panko::ArraySerializer do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
let(:foo_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :address
end
end
before { stub_const("FooSerializer", foo_serializer_class) }
it "throws argument error when each_serializer isnt passed" do
expect do
Panko::ArraySerializer.new([])
end.to raise_error(ArgumentError)
end
context "sanity" do
it "serializers array of elements" do
array_serializer_factory = -> { Panko::ArraySerializer.new([], each_serializer: FooSerializer) }
foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(Foo.all).to serialized_as(array_serializer_factory, [
{"name" => foo1.name, "address" => foo1.address},
{"name" => foo2.name, "address" => foo2.address}
])
end
it "serializes array of elements with virtual attribtues" do
class TestSerializerWithMethodsSerializer < Panko::Serializer
attributes :name, :address, :something, :context_fetch
def something
"#{object.name} #{object.address}"
end
def context_fetch
context[:value]
end
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
array_serializer_factory = -> {
Panko::ArraySerializer.new([],
each_serializer: TestSerializerWithMethodsSerializer,
context: {value: 6})
}
expect(Foo.all).to serialized_as(array_serializer_factory, [{"name" => foo.name,
"address" => foo.address,
"something" => "#{foo.name} #{foo.address}",
"context_fetch" => 6}])
end
end
context "filter" do
it "only" do
array_serializer_factory = -> { Panko::ArraySerializer.new([], each_serializer: FooSerializer, only: [:name]) }
foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(Foo.all).to serialized_as(array_serializer_factory, [
{"name" => foo1.name},
{"name" => foo2.name}
])
end
it "except" do
array_serializer_factory = -> { Panko::ArraySerializer.new([], each_serializer: FooSerializer, except: [:name]) }
foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(Foo.all).to serialized_as(array_serializer_factory, [
{"address" => foo1.address},
{"address" => foo2.address}
])
end
end
end
================================================
FILE: spec/features/associations_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"
describe "Associations Serialization" do
context "has_one" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
Temping.create(:foo_holder) do
with_columns do |t|
t.string :name
t.references :foo
end
belongs_to :foo, optional: true
end
end
let(:foo_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :address
end
end
before { stub_const("FooSerializer", foo_serializer_class) }
it "serializes plain object associations" do
class PlainFooHolder
attr_accessor :name, :foo
def initialize(name, foo)
@name = name
@foo = foo
end
end
class PlainFoo
attr_accessor :name, :address
def initialize(name, address)
@name = name
@address = address
end
end
class FooSerializer < Panko::Serializer
attributes :name, :address
end
class PlainFooHolderHasOneSerializer < Panko::Serializer
attributes :name
has_one :foo, serializer: FooSerializer
end
foo = PlainFoo.new(Faker::Lorem.word, Faker::Lorem.word)
foo_holder = PlainFooHolder.new(Faker::Lorem.word, foo)
expect(foo_holder).to serialized_as(PlainFooHolderHasOneSerializer, "name" => foo_holder.name,
"foo" => {
"name" => foo.name,
"address" => foo.address
})
end
it "accepts serializer name as string" do
class FooHolderHasOneWithStringSerializer < Panko::Serializer
attributes :name
has_one :foo, serializer: "FooSerializer"
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)
expect(foo_holder).to serialized_as(FooHolderHasOneWithStringSerializer, "name" => foo_holder.name,
"foo" => {
"name" => foo.name,
"address" => foo.address
})
end
it "accepts name option" do
class FooHolderHasOneWithNameSerializer < Panko::Serializer
attributes :name
has_one :foo, serializer: FooSerializer, name: :my_foo
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)
expect(foo_holder).to serialized_as(FooHolderHasOneWithNameSerializer, "name" => foo_holder.name,
"my_foo" => {
"name" => foo.name,
"address" => foo.address
})
end
it "serializes using the :serializer option" do
class FooHolderHasOneSerializer < Panko::Serializer
attributes :name
has_one :foo, serializer: FooSerializer
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)
expect(foo_holder).to serialized_as(FooHolderHasOneSerializer, "name" => foo_holder.name,
"foo" => {
"name" => foo.name,
"address" => foo.address
})
end
it "infers the serializer name by name of the relationship" do
class FooHolderHasOneSerializer < Panko::Serializer
attributes :name
has_one :foo
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)
expect(foo_holder).to serialized_as(FooHolderHasOneSerializer, "name" => foo_holder.name,
"foo" => {
"name" => foo.name,
"address" => foo.address
})
end
it "raises if it can't find the serializer" do
expect do
class NotFoundHasOneSerializer < Panko::Serializer
attributes :name
has_one :not_existing_serializer
end
end.to raise_error("Can't find serializer for NotFoundHasOneSerializer.not_existing_serializer has_one relationship.")
end
it "allows virtual method in has one serializer" do
class VirtualSerializer < Panko::Serializer
attributes :virtual
def virtual
"Hello #{object.name}"
end
end
class FooHolderHasOneVirtualSerializer < Panko::Serializer
attributes :name
has_one :foo, serializer: VirtualSerializer
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)
expect(foo_holder).to serialized_as(FooHolderHasOneVirtualSerializer, "name" => foo_holder.name,
"foo" => {
"virtual" => "Hello #{foo.name}"
})
end
it "handles nil" do
class FooHolderHasOneSerializer < Panko::Serializer
attributes :name
has_one :foo, serializer: FooSerializer
end
foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: nil)
expect(foo_holder).to serialized_as(FooHolderHasOneSerializer, "name" => foo_holder.name,
"foo" => nil)
end
end
context "has_one with different model types" do
it "can use the serializer string name when resolving the serializer" do
Temping.create(:goo) do
with_columns do |t|
t.string :name
t.string :address
end
end
Temping.create(:foo_holder) do
with_columns do |t|
t.string :name
t.references :goo
end
belongs_to :goo
end
class FooSerializer < Panko::Serializer
attributes :name, :address
end
class FooHolderHasOnePooWithStringSerializer < Panko::Serializer
attributes :name
has_one :goo, serializer: "FooSerializer"
end
goo = Goo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo_holder = FooHolder.create(name: Faker::Lorem.word, goo: goo)
expect(foo_holder).to serialized_as(
FooHolderHasOnePooWithStringSerializer,
"name" => foo_holder.name,
"goo" => {
"name" => goo.name,
"address" => goo.address
}
)
end
end
context "has_many" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
t.references :foos_holder
end
belongs_to :foos_holder, optional: true
end
Temping.create(:foos_holder) do
with_columns do |t|
t.string :name
end
has_many :foos
end
end
let(:foo_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :address
end
end
before { stub_const("FooSerializer", foo_serializer_class) }
it "serializes using the :serializer option" do
class FoosHasManyHolderSerializer < Panko::Serializer
attributes :name
has_many :foos, serializer: FooSerializer
end
foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])
expect(foos_holder).to serialized_as(FoosHasManyHolderSerializer, "name" => foos_holder.name,
"foos" => [
{
"name" => foo1.name,
"address" => foo1.address
},
{
"name" => foo2.name,
"address" => foo2.address
}
])
end
it "accepts serializer name as string" do
class FoosHasManyHolderSerializer < Panko::Serializer
attributes :name
has_many :foos, serializer: "FooSerializer"
end
foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])
expect(foos_holder).to serialized_as(FoosHasManyHolderSerializer, "name" => foos_holder.name,
"foos" => [
{
"name" => foo1.name,
"address" => foo1.address
},
{
"name" => foo2.name,
"address" => foo2.address
}
])
end
it "supports :name" do
class FoosHasManyHolderWithNameSerializer < Panko::Serializer
attributes :name
has_many :foos, serializer: FooSerializer, name: :my_foos
end
foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])
expect(foos_holder).to serialized_as(FoosHasManyHolderWithNameSerializer, "name" => foos_holder.name,
"my_foos" => [
{
"name" => foo1.name,
"address" => foo1.address
},
{
"name" => foo2.name,
"address" => foo2.address
}
])
end
it "infers the serializer name by name of the relationship" do
class FoosHasManyHolderSerializer < Panko::Serializer
attributes :name
has_many :foos
end
foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])
expect(foos_holder).to serialized_as(FoosHasManyHolderSerializer, "name" => foos_holder.name,
"foos" => [
{
"name" => foo1.name,
"address" => foo1.address
},
{
"name" => foo2.name,
"address" => foo2.address
}
])
end
it "raises if it can't find the serializer" do
expect do
class NotFoundHasManySerializer < Panko::Serializer
attributes :name
has_many :not_existing_serializers
end
end.to raise_error("Can't find serializer for NotFoundHasManySerializer.not_existing_serializers has_many relationship.")
end
it "serializes using the :each_serializer option" do
class FoosHasManyHolderSerializer < Panko::Serializer
attributes :name
has_many :foos, each_serializer: FooSerializer
end
foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])
expect(foos_holder).to serialized_as(FoosHasManyHolderSerializer, "name" => foos_holder.name,
"foos" => [
{
"name" => foo1.name,
"address" => foo1.address
},
{
"name" => foo2.name,
"address" => foo2.address
}
])
end
it "accepts only as option" do
class FoosHolderWithOnlySerializer < Panko::Serializer
attributes :name
has_many :foos, serializer: FooSerializer, only: [:address]
end
foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])
expect(foos_holder).to serialized_as(FoosHolderWithOnlySerializer, "name" => foos_holder.name,
"foos" => [
{
"address" => foo1.address
},
{
"address" => foo2.address
}
])
end
end
context "has_many with different model types" do
it "uses the serializer string name when resolving the serializer" do
Temping.create(:goo) do
with_columns do |t|
t.string :name
t.string :address
t.references :foos_holder
end
belongs_to :foos_holder, optional: true
end
Temping.create(:foos_holder) do
with_columns do |t|
t.string :name
end
has_many :goos
end
class FooSerializer < Panko::Serializer
attributes :name, :address
end
class FoosHasManyPoosHolderSerializer < Panko::Serializer
attributes :name
has_many :goos, serializer: "FooSerializer"
end
goo1 = Goo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
goo2 = Goo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foos_holder = FoosHolder.create(name: Faker::Lorem.word, goos: [goo1, goo2])
expect(foos_holder).to serialized_as(
FoosHasManyPoosHolderSerializer,
"name" => foos_holder.name,
"goos" => [
{
"name" => goo1.name,
"address" => goo1.address
},
{
"name" => goo2.name,
"address" => goo2.address
}
]
)
end
end
context "polymorphic associations" do
it "serializes polymorphic has_many associations" do
Temping.create(:comment) do
with_columns do |t|
t.string :content
t.references :commentable, polymorphic: true
end
belongs_to :commentable, polymorphic: true
end
Temping.create(:post) do
with_columns do |t|
t.string :title
t.string :content
end
has_many :comments, as: :commentable
end
class CommentSerializer < Panko::Serializer
attributes :content
end
class PostSerializer < Panko::Serializer
attributes :title, :content
has_many :comments, serializer: CommentSerializer
end
post = Post.create(title: Faker::Lorem.word, content: Faker::Lorem.sentence)
comment1 = Comment.create(content: Faker::Lorem.sentence, commentable: post)
comment2 = Comment.create(content: Faker::Lorem.sentence, commentable: post)
expect(post).to serialized_as(PostSerializer,
"title" => post.title,
"content" => post.content,
"comments" => [
{"content" => comment1.content},
{"content" => comment2.content}
])
end
it "serializes polymorphic associations with different models" do
Temping.create(:comment) do
with_columns do |t|
t.string :content
t.references :commentable, polymorphic: true
end
belongs_to :commentable, polymorphic: true
end
Temping.create(:article) do
with_columns do |t|
t.string :title
t.string :body
end
has_many :comments, as: :commentable
end
class CommentSerializer < Panko::Serializer
attributes :content
end
class ArticleSerializer < Panko::Serializer
attributes :title, :body
has_many :comments, serializer: CommentSerializer
end
article = Article.create(title: Faker::Lorem.word, body: Faker::Lorem.sentence)
comment = Comment.create(content: Faker::Lorem.sentence, commentable: article)
expect(article).to serialized_as(ArticleSerializer,
"title" => article.title,
"body" => article.body,
"comments" => [
{"content" => comment.content}
])
end
end
context "deeply nested associations" do
it "serializes 2+ levels of nesting" do
Temping.create(:user) do
with_columns do |t|
t.string :name
t.string :email
t.references :team
end
belongs_to :team
end
Temping.create(:team) do
with_columns do |t|
t.string :name
t.references :organization
end
belongs_to :organization
has_many :users
end
Temping.create(:organization) do
with_columns do |t|
t.string :name
end
has_many :teams
has_many :users, through: :teams
end
class UserSerializer < Panko::Serializer
attributes :name, :email
end
class TeamSerializer < Panko::Serializer
attributes :name
has_many :users, serializer: UserSerializer
end
class OrganizationSerializer < Panko::Serializer
attributes :name
has_many :teams, serializer: TeamSerializer
end
org = Organization.create(name: Faker::Company.name)
team1 = Team.create(name: Faker::Team.name, organization: org)
team2 = Team.create(name: Faker::Team.name, organization: org)
user1 = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team1)
user2 = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team2)
expect(org).to serialized_as(OrganizationSerializer,
"name" => org.name,
"teams" => [
{
"name" => team1.name,
"users" => [
{
"name" => user1.name,
"email" => user1.email
}
]
},
{
"name" => team2.name,
"users" => [
{
"name" => user2.name,
"email" => user2.email
}
]
}
])
end
end
context "combined options" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
t.references :foos_holder
end
belongs_to :foos_holder, optional: true
end
Temping.create(:foos_holder) do
with_columns do |t|
t.string :name
end
has_many :foos
end
Temping.create(:foo_holder) do
with_columns do |t|
t.string :name
t.references :foo
end
belongs_to :foo
end
end
let(:foo_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :address
end
end
before { stub_const("FooSerializer", foo_serializer_class) }
it "handles multiple options together (name, serializer, only)" do
class FoosHolderCombinedOptionsSerializer < Panko::Serializer
attributes :name
has_many :foos, serializer: FooSerializer, name: :my_items, only: [:name]
end
foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])
expect(foos_holder).to serialized_as(FoosHolderCombinedOptionsSerializer,
"name" => foos_holder.name,
"my_items" => [
{"name" => foo1.name},
{"name" => foo2.name}
])
end
it "handles has_one with multiple options" do
class FooHolderCombinedOptionsSerializer < Panko::Serializer
attributes :name
has_one :foo, serializer: FooSerializer, name: :my_foo
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)
expect(foo_holder).to serialized_as(FooHolderCombinedOptionsSerializer,
"name" => foo_holder.name,
"my_foo" => {
"name" => foo.name,
"address" => foo.address
})
end
end
context "nil and empty associations" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
t.references :foos_holder
end
belongs_to :foos_holder, optional: true
end
Temping.create(:foo_holder) do
with_columns do |t|
t.string :name
t.references :foo
end
belongs_to :foo, optional: true
end
Temping.create(:foos_holder) do
with_columns do |t|
t.string :name
end
has_many :foos
end
end
let(:foo_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :address
end
end
before { stub_const("FooSerializer", foo_serializer_class) }
it "explicitly handles has_one returning nil" do
class FooHolderNilSerializer < Panko::Serializer
attributes :name
has_one :foo, serializer: FooSerializer
end
foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: nil)
expect(foo_holder).to serialized_as(FooHolderNilSerializer,
"name" => foo_holder.name,
"foo" => nil)
end
it "explicitly handles has_many returning empty array" do
class FoosHolderEmptySerializer < Panko::Serializer
attributes :name
has_many :foos, serializer: FooSerializer
end
foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [])
expect(foos_holder).to serialized_as(FoosHolderEmptySerializer,
"name" => foos_holder.name,
"foos" => [])
end
end
context "invalid associations" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
Temping.create(:foo_holder) do
with_columns do |t|
t.string :name
t.references :foo
end
belongs_to :foo
end
end
it "handles when associated object is not of expected type" do
# This test verifies graceful handling when an association returns an unexpected type
class FlexibleSerializer < Panko::Serializer
attributes :name, :address
def name
# Handle case where object might not respond to name
object.respond_to?(:name) ? object.name : "unknown"
end
def address
object.respond_to?(:address) ? object.address : "unknown"
end
end
class FooHolderFlexibleSerializer < Panko::Serializer
attributes :name
has_one :foo, serializer: FlexibleSerializer
end
# Create a foo_holder with a regular foo
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)
expect(foo_holder).to serialized_as(FooHolderFlexibleSerializer,
"name" => foo_holder.name,
"foo" => {
"name" => foo.name,
"address" => foo.address
})
end
end
end
================================================
FILE: spec/features/attributes_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"
describe "Attributes Serialization" do
context "instance variables" do
it "serializes instance variables" do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
class FooSerializer < Panko::Serializer
attributes :name, :address
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(foo).to serialized_as(FooSerializer,
"name" => foo.name,
"address" => foo.address)
end
end
context "method attributes" do
it "serializes method attributes" do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
class FooWithMethodsSerializer < Panko::Serializer
attributes :name, :address, :something
def something
"#{object.name} #{object.address}"
end
def another_method
raise "I shouldn't get called"
end
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(foo).to serialized_as(FooWithMethodsSerializer, "name" => foo.name,
"address" => foo.address,
"something" => "#{foo.name} #{foo.address}")
end
end
context "inheritance" do
it "supports serializer inheritance" do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
class BaseSerializer < Panko::Serializer
attributes :name
end
class ChildSerializer < BaseSerializer
attributes :address
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(foo).to serialized_as(ChildSerializer, "name" => foo.name,
"address" => foo.address)
end
end
context "time serialization" do
it "serializes time correctly" do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
t.timestamps
end
end
class ObjectWithTimeSerializer < Panko::Serializer
attributes :created_at, :method
def method
object.created_at
end
end
obj = Foo.create
expect(obj).to serialized_as(ObjectWithTimeSerializer,
"created_at" => obj.created_at.as_json,
"method" => obj.created_at.as_json)
end
end
context "type handling" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :value
end
end
end
it "honors additional types" do
class FooValueSerializer < Panko::Serializer
attributes :value
end
foo = Foo.instantiate({"value" => "1"},
"value" => ActiveRecord::Type::Integer.new)
expect(foo).to serialized_as(FooValueSerializer, "value" => 1)
end
end
context "aliases" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
it "supports active record alias attributes" do
class FooWithAliasesModel < ActiveRecord::Base
self.table_name = "foos"
alias_attribute :full_name, :name
end
class FooWithArAliasesSerializer < Panko::Serializer
attributes :full_name, :address
end
foo = FooWithAliasesModel.create(full_name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(foo).to serialized_as(FooWithArAliasesSerializer, "full_name" => foo.name, "address" => foo.address)
end
it "allows to alias attributes" do
class FooWithAliasesSerializer < Panko::Serializer
attributes :address
aliases name: :full_name
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(foo).to serialized_as(FooWithAliasesSerializer, "full_name" => foo.name, "address" => foo.address)
end
context "alias with method_fields" do
let(:data) { {"created_at" => created_at} }
let(:created_at) { "2023-04-18T09:24:41+00:00" }
context "with alias" do
let(:serializer_class) do
Class.new(Panko::Serializer) do
aliases({created_at: :createdAt})
end
end
it "has createdAt" do
expect(data).to serialized_as(serializer_class,
"createdAt" => created_at)
end
end
context "with alias + method_fields" do
let(:serializer_class) do
Class.new(Panko::Serializer) do
aliases({created_at: :createdAt})
def created_at
"2023-04-18T09:24:41+00:00"
end
end
end
it "has createdAt" do
expect(data).to serialized_as(serializer_class,
"createdAt" => created_at)
end
end
end
end
context "null values" do
it "serializes null values" do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
class FooSerializer < Panko::Serializer
attributes :name, :address
end
expect(Foo.create).to serialized_as(FooSerializer, "name" => nil, "address" => nil)
end
end
context "SKIP functionality" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
it "can skip fields" do
class FooSkipSerializer < FooSerializer
def address
object.address || SKIP
end
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(foo).to serialized_as(FooSkipSerializer, "name" => foo.name, "address" => foo.address)
foo = Foo.create(name: Faker::Lorem.word, address: nil)
expect(foo).to serialized_as(FooSkipSerializer, "name" => foo.name)
end
end
context "serializer reuse" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
it "raises an error when reusing serializer instances" do
class FooSerializer < Panko::Serializer
attributes :name, :address
end
serializer = FooSerializer.new
foo_a = Foo.create
foo_b = Foo.create
expect { serializer.serialize(foo_a) }.not_to raise_error
expect { serializer.serialize(foo_b) }.to raise_error(ArgumentError, "Panko::Serializer instances are single-use")
end
end
end
================================================
FILE: spec/features/context_and_scope_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"
describe "Context and Scope" do
context "context" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
it "passes context to attribute methods" do
class FooWithContextSerializer < Panko::Serializer
attributes :name, :context_value
def context_value
context[:value]
end
end
context = {value: Faker::Lorem.word}
serializer_factory = -> { FooWithContextSerializer.new(context: context) }
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(foo).to serialized_as(serializer_factory,
"name" => foo.name,
"context_value" => context[:value])
end
end
context "scope" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
Temping.create(:foo_holder) do
with_columns do |t|
t.string :name
t.references :foo
end
belongs_to :foo
end
end
let(:foo_with_scope_serializer_class) do
Class.new(Panko::Serializer) do
attributes :scope_value
def scope_value
scope
end
end
end
let(:foo_holder_with_scope_serializer_class) do
Class.new(Panko::Serializer) do
attributes :scope_value
has_one :foo, serializer: "FooWithScopeSerializer"
def scope_value
scope
end
end
end
before do
stub_const("FooWithScopeSerializer", foo_with_scope_serializer_class)
stub_const("FooHolderWithScopeSerializer", foo_holder_with_scope_serializer_class)
end
it "passes scope to attribute methods" do
scope = 123
serializer_factory = -> { FooHolderWithScopeSerializer.new(scope: scope) }
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)
expect(foo_holder).to serialized_as(serializer_factory,
"scope_value" => scope,
"foo" => {
"scope_value" => scope
})
end
it "default scope is nil" do
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)
expect(foo_holder).to serialized_as(FooHolderWithScopeSerializer, "scope_value" => nil,
"foo" => {
"scope_value" => nil
})
end
end
end
================================================
FILE: spec/features/filtering_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"
describe "Filtering Serialization" do
context "basic filtering" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
let(:foo_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :address
end
end
before { stub_const("FooSerializer", foo_serializer_class) }
it "supports only filter" do
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(foo).to serialized_as(-> { FooSerializer.new(only: [:name]) }, "name" => foo.name)
end
it "supports except filter" do
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(foo).to serialized_as(-> { FooSerializer.new(except: [:name]) }, "address" => foo.address)
end
end
context "association filtering" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
t.references :foos_holder
end
belongs_to :foos_holder, optional: true
end
Temping.create(:foos_holder) do
with_columns do |t|
t.string :name
end
has_many :foos
end
end
let(:foo_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :address
end
end
before { stub_const("FooSerializer", foo_serializer_class) }
it "filters associations" do
class FoosHolderForFilterTestSerializer < Panko::Serializer
attributes :name
has_many :foos, serializer: FooSerializer
end
serializer_factory = -> { FoosHolderForFilterTestSerializer.new(only: [:foos]) }
foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])
expect(foos_holder).to serialized_as(serializer_factory, "foos" => [
{
"name" => foo1.name,
"address" => foo1.address
},
{
"name" => foo2.name,
"address" => foo2.address
}
])
end
it "filters association attributes" do
class FoosHolderForFilterTestSerializer < Panko::Serializer
attributes :name
has_many :foos, serializer: FooSerializer
end
serializer_factory = -> { FoosHolderForFilterTestSerializer.new(only: {foos: [:name]}) }
foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])
expect(foos_holder).to serialized_as(serializer_factory, "name" => foos_holder.name,
"foos" => [
{
"name" => foo1.name
},
{
"name" => foo2.name
}
])
end
end
context "complex nested filters" do
before do
Temping.create(:user) do
with_columns do |t|
t.string :name
t.string :email
t.references :team
end
belongs_to :team
end
Temping.create(:team) do
with_columns do |t|
t.string :name
t.references :organization
end
belongs_to :organization
has_many :users
end
Temping.create(:organization) do
with_columns do |t|
t.string :name
end
has_many :teams
has_many :users, through: :teams
end
end
let(:user_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :email
end
end
let(:team_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name
has_many :users, serializer: "UserSerializer"
end
end
let(:organization_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name
has_many :teams, serializer: "TeamSerializer"
end
end
before do
stub_const("UserSerializer", user_serializer_class)
stub_const("TeamSerializer", team_serializer_class)
stub_const("OrganizationSerializer", organization_serializer_class)
end
it "supports complex nested only clauses on has_many" do
org = Organization.create(name: Faker::Company.name)
team = Team.create(name: Faker::Team.name, organization: org)
user = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team)
serializer_factory = -> { OrganizationSerializer.new(only: {teams: {users: [:email]}}) }
expect(org).to serialized_as(serializer_factory,
"name" => org.name,
"teams" => [
{
"name" => team.name,
"users" => [
{"email" => user.email}
]
}
])
end
it "supports complex nested only clauses on has_one" do
class TeamWithOrgSerializer < Panko::Serializer
attributes :name
has_one :organization, serializer: OrganizationSerializer
end
org = Organization.create(name: Faker::Company.name)
team = Team.create(name: Faker::Team.name, organization: org)
serializer_factory = -> { TeamWithOrgSerializer.new(only: {organization: [:name]}) }
expect(team).to serialized_as(serializer_factory,
"name" => team.name,
"organization" => {
"name" => org.name
})
end
end
context "aliased attribute filtering" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
it "filters based on aliased attribute name" do
class FooWithAliasFilterSerializer < Panko::Serializer
attributes :address
aliases name: :full_name
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
# Filter using the alias name
expect(foo).to serialized_as(-> { FooWithAliasFilterSerializer.new(only: [:full_name]) },
"full_name" => foo.name)
# Filter using except should also work
expect(foo).to serialized_as(-> { FooWithAliasFilterSerializer.new(except: [:address]) },
"full_name" => foo.name)
end
end
context "filter interactions" do
before do
Temping.create(:user) do
with_columns do |t|
t.string :name
t.string :email
t.references :team
end
belongs_to :team
end
Temping.create(:team) do
with_columns do |t|
t.string :name
end
has_many :users
end
end
let(:user_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :email
end
end
before { stub_const("UserSerializer", user_serializer_class) }
it "tests interaction between ArraySerializer filters and has_many association filters" do
class TeamArrayFilterSerializer < Panko::Serializer
attributes :name
has_many :users, serializer: UserSerializer
end
team = Team.create(name: Faker::Team.name)
user1 = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team)
user2 = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team)
# ArraySerializer filter should work with association filters
array_serializer = Panko::ArraySerializer.new([team], each_serializer: TeamArrayFilterSerializer, only: {users: [:name]})
result = array_serializer.to_json
expected = [
{
"name" => team.name,
"users" => [
{"name" => user1.name},
{"name" => user2.name}
]
}
]
expect(JSON.parse(result)).to eq(expected)
end
end
context "nested except filters" do
before do
Temping.create(:user) do
with_columns do |t|
t.string :name
t.string :email
t.references :team
end
belongs_to :team
end
Temping.create(:team) do
with_columns do |t|
t.string :name
end
has_many :users
end
end
let(:user_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :email
end
end
before { stub_const("UserSerializer", user_serializer_class) }
it "supports nested except filters" do
class TeamWithExceptSerializer < Panko::Serializer
attributes :name
has_many :users, serializer: UserSerializer
end
team = Team.create(name: Faker::Team.name)
user = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team)
serializer_factory = -> { TeamWithExceptSerializer.new(except: {users: [:email]}) }
expect(team).to serialized_as(serializer_factory,
"name" => team.name,
"users" => [
{"name" => user.name}
])
end
end
context "only vs except precedence" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
let(:foo_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :address
end
end
before { stub_const("FooSerializer", foo_serializer_class) }
it "only takes precedence over except when both are provided" do
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(foo).to serialized_as(-> { FooSerializer.new(only: [:name], except: [:address]) }, "name" => foo.name)
end
end
context "filters_for interaction" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
it "combines filters_for with constructor only/except options" do
class FooWithFiltersForSerializer < Panko::Serializer
attributes :name, :address
def self.filters_for(context, scope)
{except: [:address]}
end
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
# filters_for except should be combined with constructor only
expect(foo).to serialized_as(-> { FooWithFiltersForSerializer.new(only: [:name]) }, "name" => foo.name)
end
it "passes context and scope to filters_for method" do
class FooWithContextFiltersSerializer < Panko::Serializer
attributes :name, :address
def self.filters_for(context, scope)
if context[:user_role] == "admin"
{only: [:name, :address]}
else
{only: [:name]}
end
end
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
# Test with admin context
expect(foo).to serialized_as(-> { FooWithContextFiltersSerializer.new(context: {user_role: "admin"}) },
"name" => foo.name, "address" => foo.address)
# Test with regular user context
expect(foo).to serialized_as(-> { FooWithContextFiltersSerializer.new(context: {user_role: "user"}) },
"name" => foo.name)
end
end
context "deeply nested filters" do
before do
Temping.create(:user) do
with_columns do |t|
t.string :name
t.string :email
t.references :team
end
belongs_to :team
end
Temping.create(:team) do
with_columns do |t|
t.string :name
t.references :organization
end
belongs_to :organization
has_many :users
end
Temping.create(:organization) do
with_columns do |t|
t.string :name
end
has_many :teams
has_many :users, through: :teams
end
end
let(:user_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :email
end
end
let(:team_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name
has_many :users, serializer: "UserSerializer"
end
end
let(:organization_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name
has_many :teams, serializer: "TeamSerializer"
end
end
before do
stub_const("UserSerializer", user_serializer_class)
stub_const("TeamSerializer", team_serializer_class)
stub_const("OrganizationSerializer", organization_serializer_class)
end
it "filters on 2+ levels deep associations" do
org = Organization.create(name: Faker::Company.name)
team = Team.create(name: Faker::Team.name, organization: org)
user = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team)
serializer_factory = -> { OrganizationSerializer.new(only: {teams: {users: [:name]}}) }
expect(org).to serialized_as(serializer_factory,
"name" => org.name,
"teams" => [
{
"name" => team.name,
"users" => [
{"name" => user.name}
]
}
])
end
it "supports mixed only and except at different nesting levels" do
org = Organization.create(name: Faker::Company.name)
team = Team.create(name: Faker::Team.name, organization: org)
user = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team)
# Use only at top level, except at nested level
serializer_factory = -> { OrganizationSerializer.new(only: [:teams], except: {teams: {users: [:email]}}) }
expect(org).to serialized_as(serializer_factory,
"teams" => [
{
"name" => team.name,
"users" => [
{"name" => user.name}
]
}
])
end
end
context "invalid filter values" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
let(:foo_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :address
end
end
before { stub_const("FooSerializer", foo_serializer_class) }
it "raises error for non-Array/Hash only filters" do
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect do
FooSerializer.new(only: "invalid").serialize(foo)
end.to raise_error(NoMethodError)
# TODO: change the error to be ArgumentError
end
it "raises error for non-Array/Hash except filters" do
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect do
FooSerializer.new(except: 123).serialize(foo)
end.to raise_error(NoMethodError)
# TODO: change the error to be ArgumentError
end
it "handles filters on non-existent attributes" do
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
# Should not raise error, just ignore non-existent attributes
expect(foo).to serialized_as(-> { FooSerializer.new(only: [:name, :non_existent]) }, "name" => foo.name)
end
it "handles filters on non-existent associations" do
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
serializer_factory = -> { FooSerializer.new(only: {non_existent_association: [:name]}) }
expect(foo).to serialized_as(serializer_factory,
"name" => foo.name,
"address" => foo.address)
end
end
context "filters_for method" do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
it "fetches the filters from the serializer" do
class FooWithFiltersForSerializer < Panko::Serializer
attributes :name, :address
def self.filters_for(context, scope)
{only: [:name]}
end
end
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
expect(foo).to serialized_as(-> { FooWithFiltersForSerializer.new }, "name" => foo.name)
end
end
end
================================================
FILE: spec/features/hash_serialization_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"
describe "Hash Serialization" do
class FooSerializer < Panko::Serializer
attributes :name, :address
end
it "serializes hash with string keys" do
foo = {
"name" => Faker::Lorem.word,
"address" => Faker::Lorem.word
}
expect(foo).to serialized_as(FooSerializer,
"name" => foo["name"],
"address" => foo["address"])
end
it "serializes HashWithIndifferentAccess with symbol keys" do
foo = ActiveSupport::HashWithIndifferentAccess.new(
name: Faker::Lorem.word,
address: Faker::Lorem.word
)
expect(foo).to serialized_as(FooSerializer,
"name" => foo["name"],
"address" => foo["address"])
end
it "serializes HashWithIndifferentAccess with string keys" do
foo = ActiveSupport::HashWithIndifferentAccess.new(
"name" => Faker::Lorem.word,
"address" => Faker::Lorem.word
)
expect(foo).to serialized_as(FooSerializer,
"name" => foo["name"],
"address" => foo["address"])
end
end
================================================
FILE: spec/features/poro_serialization_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"
describe "PORO Serialization" do
class FooSerializer < Panko::Serializer
attributes :name, :address
end
it "serializes plain objects" do
class PlainFoo
attr_accessor :name, :address
def initialize(name, address)
@name = name
@address = address
end
end
foo = PlainFoo.new(Faker::Lorem.word, Faker::Lorem.word)
expect(foo).to serialized_as(FooSerializer,
"name" => foo.name,
"address" => foo.address)
end
end
================================================
FILE: spec/spec_helper.rb
================================================
# frozen_string_literal: true
require "bundler/setup"
require "logger"
require "panko_serializer"
require "faker"
require "active_record"
require "temping"
# Load database configuration helper
require_relative "support/database_config"
# Require database adapters based on environment
case DatabaseConfig.database_type
when "sqlite"
require "sqlite3"
when "postgresql"
require "pg"
when "mysql"
require "trilogy"
end
# Set up database connection
DatabaseConfig.setup_database
ActiveRecord::Base.establish_connection(DatabaseConfig.config)
# Don't show migration output
ActiveRecord::Migration.verbose = false
RSpec.configure do |config|
config.order = "random"
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
config.filter_run focus: true
config.run_all_when_everything_filtered = true
config.full_backtrace = ENV.fetch("CI", false)
if ENV.fetch("CI", false)
config.before(:example, :focus) do
raise "This example was committed with `:focus` and should not have been"
end
end
config.expect_with :rspec do |c|
c.syntax = :expect
end
config.after do
Temping.teardown
end
end
RSpec::Matchers.define :serialized_as do |serializer_factory_or_class, output|
serializer_factory = if serializer_factory_or_class.respond_to?(:call)
serializer_factory_or_class
else
-> { serializer_factory_or_class.new }
end
match do |object|
expect(serializer_factory.call.serialize(object)).to eq(output)
json = Oj.load serializer_factory.call.serialize_to_json(object)
expect(json).to eq(output)
end
failure_message do |object|
<<~FAILURE
Expected Output:
#{output}
Got:
Object: #{serializer_factory.call.serialize(object)}
JSON: #{Oj.load(serializer_factory.call.serialize_to_json(object))}
FAILURE
end
end
if GC.respond_to?(:verify_compaction_references)
# This method was added in Ruby 3.0.0. Calling it this way asks the GC to
# move objects around, helping to find object movement bugs.
GC.verify_compaction_references(double_heap: true, toward: :empty)
end
================================================
FILE: spec/support/database_config.rb
================================================
# frozen_string_literal: true
# Database configuration helper for tests
class DatabaseConfig
ADAPTERS = {
"sqlite" => {
adapter: "sqlite3",
database: ":memory:"
},
"postgresql" => {
adapter: "postgresql",
database: "panko_test",
host: ENV["POSTGRES_HOST"] || "localhost",
username: ENV["POSTGRES_USER"] || "postgres",
password: ENV["POSTGRES_PASSWORD"] || "",
port: ENV["POSTGRES_PORT"] || 5432
},
"mysql" => {
adapter: "trilogy",
database: "panko_test",
host: ENV["MYSQL_HOST"] || "localhost",
username: ENV["MYSQL_USER"] || "root",
password: ENV["MYSQL_PASSWORD"] || "",
port: ENV["MYSQL_PORT"] || 3306
}
}.freeze
def self.database_type
ENV["DB"] || "sqlite"
end
def self.config
adapter_config = ADAPTERS[database_type]
raise "Unsupported database type: #{database_type}. Supported: #{ADAPTERS.keys.join(", ")}" unless adapter_config
adapter_config
end
def self.setup_database
# For CI and local development, we assume databases are already created
# SQLite uses in-memory database which needs no setup
# PostgreSQL and MySQL databases should be created externally
puts "Using #{database_type} database: #{config[:database]}" if ENV["DEBUG"]
end
def self.teardown_database
# For SQLite in-memory, no teardown needed
# For persistent databases, we rely on test transaction rollbacks
# rather than dropping/recreating the database for performance
end
end
================================================
FILE: spec/unit/panko/array_serializer_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"
describe Panko::ArraySerializer do
describe "#initialize" do
it "raises ArgumentError when each_serializer is not provided" do
expect do
Panko::ArraySerializer.new([])
end.to raise_error(ArgumentError, /Please pass valid each_serializer/)
end
it "accepts each_serializer option" do
mock_serializer = Class.new(Panko::Serializer)
expect do
Panko::ArraySerializer.new([], each_serializer: mock_serializer)
end.not_to raise_error
end
it "stores subjects" do
mock_serializer = Class.new(Panko::Serializer)
subjects = [1, 2, 3]
array_serializer = Panko::ArraySerializer.new(subjects, each_serializer: mock_serializer)
expect(array_serializer.subjects).to eq(subjects)
end
it "builds serialization context from options" do
mock_serializer = Class.new(Panko::Serializer)
context = {user_id: 123}
scope = "admin"
array_serializer = Panko::ArraySerializer.new([],
each_serializer: mock_serializer,
context: context,
scope: scope)
serialization_context = array_serializer.instance_variable_get(:@serialization_context)
expect(serialization_context.context).to eq(context)
expect(serialization_context.scope).to eq(scope)
end
it "passes filtering options to descriptor" do
mock_serializer = Class.new(Panko::Serializer)
# Mock SerializationDescriptor.build to verify options are passed
expect(Panko::SerializationDescriptor).to receive(:build).with(
mock_serializer,
hash_including(
only: [:name],
except: [:email],
context: {user_id: 123},
scope: "admin"
),
anything
)
Panko::ArraySerializer.new([],
each_serializer: mock_serializer,
only: [:name],
except: [:email],
context: {user_id: 123},
scope: "admin")
end
it "defaults only and except to empty arrays when not provided" do
mock_serializer = Class.new(Panko::Serializer)
expect(Panko::SerializationDescriptor).to receive(:build).with(
mock_serializer,
hash_including(
only: [],
except: []
),
anything
)
Panko::ArraySerializer.new([], each_serializer: mock_serializer)
end
end
describe "option handling" do
let(:mock_serializer) { Class.new(Panko::Serializer) }
it "handles only option" do
array_serializer = Panko::ArraySerializer.new([],
each_serializer: mock_serializer,
only: [:name, :email])
# Verify that the descriptor was built with the only option
descriptor = array_serializer.instance_variable_get(:@descriptor)
expect(descriptor).not_to be_nil
end
it "handles except option" do
array_serializer = Panko::ArraySerializer.new([],
each_serializer: mock_serializer,
except: [:password, :secret])
descriptor = array_serializer.instance_variable_get(:@descriptor)
expect(descriptor).not_to be_nil
end
it "handles context option" do
context = {current_user: "admin"}
array_serializer = Panko::ArraySerializer.new([],
each_serializer: mock_serializer,
context: context)
serialization_context = array_serializer.instance_variable_get(:@serialization_context)
expect(serialization_context.context).to eq(context)
end
it "handles scope option" do
scope = "public"
array_serializer = Panko::ArraySerializer.new([],
each_serializer: mock_serializer,
scope: scope)
serialization_context = array_serializer.instance_variable_get(:@serialization_context)
expect(serialization_context.scope).to eq(scope)
end
end
describe "serialization methods" do
let(:mock_serializer) { Class.new(Panko::Serializer) }
let(:subjects) { [double("obj1"), double("obj2")] }
let(:array_serializer) { Panko::ArraySerializer.new(subjects, each_serializer: mock_serializer) }
describe "#serialize" do
it "calls Panko.serialize_objects with correct parameters" do
mock_writer = double("writer", output: [])
allow(Panko::ObjectWriter).to receive(:new).and_return(mock_writer)
expect(Panko).to receive(:serialize_objects).with(
subjects,
mock_writer,
array_serializer.instance_variable_get(:@descriptor)
)
array_serializer.serialize(subjects)
end
end
describe "#to_a" do
it "calls serialize_with_writer with stored subjects" do
mock_writer = double("writer", output: [])
allow(Panko::ObjectWriter).to receive(:new).and_return(mock_writer)
expect(Panko).to receive(:serialize_objects).with(
subjects,
mock_writer,
anything
)
array_serializer.to_a
end
end
describe "#serialize_to_json" do
it "uses Oj::StringWriter for JSON output" do
mock_writer = double("writer", to_s: "[]")
allow(Oj::StringWriter).to receive(:new).with(mode: :rails).and_return(mock_writer)
allow(Panko).to receive(:serialize_objects)
result = array_serializer.serialize_to_json(subjects)
expect(result).to eq("[]")
end
end
describe "#to_json" do
it "calls serialize_to_json with stored subjects" do
expect(array_serializer).to receive(:serialize_to_json).with(subjects)
array_serializer.to_json
end
end
end
end
================================================
FILE: spec/unit/panko/object_writer_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"
require "active_record/connection_adapters/postgresql_adapter"
describe Panko::ObjectWriter do
let(:writer) { Panko::ObjectWriter.new }
context "push_object" do
it "property" do
writer.push_object
writer.push_value "yosi", "name"
writer.pop
expect(writer.output).to eql("name" => "yosi")
end
it "property with separate key value instructions" do
writer.push_object
writer.push_key "name"
writer.push_value "yosi"
writer.pop
expect(writer.output).to eql("name" => "yosi")
end
it "supports nested objects" do
writer.push_object
writer.push_value "yosi", "name"
writer.push_object("nested")
writer.push_value "key1", "value"
writer.pop
writer.pop
expect(writer.output).to eql(
"name" => "yosi",
"nested" => {
"value" => "key1"
}
)
end
it "supports nested arrays" do
writer.push_object
writer.push_value "yosi", "name"
writer.push_object("nested")
writer.push_value "key1", "value"
writer.pop
writer.push_array "values"
writer.push_object
writer.push_value "item", "key"
writer.pop
writer.pop
writer.pop
expect(writer.output).to eql(
"name" => "yosi",
"nested" => {
"value" => "key1"
},
"values" => [
{"key" => "item"}
]
)
end
end
it "supports arrays" do
writer.push_array
writer.push_object
writer.push_value "key1", "value"
writer.pop
writer.push_object
writer.push_value "key2", "value2"
writer.pop
writer.pop
expect(writer.output).to eql([
{
"value" => "key1"
},
{
"value2" => "key2"
}
])
end
end
================================================
FILE: spec/unit/panko/response_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"
describe Panko::Response do
before do
Temping.create(:foo) do
with_columns do |t|
t.string :name
t.string :address
end
end
end
let(:foo_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :address
end
end
let(:foo_with_context_serializer_class) do
Class.new(Panko::Serializer) do
attributes :name, :context_value
def context_value
context[:value]
end
end
end
before do
stub_const("FooSerializer", foo_serializer_class)
stub_const("FooWithContextSerializer", foo_with_context_serializer_class)
end
it "serializes primitive values" do
response = Panko::Response.new(success: true, num: 1)
json_response = Oj.load(response.to_json)
expect(json_response["success"]).to eq(true)
expect(json_response["num"]).to eq(1)
end
it "serializes hash values" do
hash = {"a" => 1, "b" => 2}
response = Panko::Response.new(success: true, hash: hash)
json_response = Oj.load(response.to_json)
expect(json_response["hash"]).to eq(hash)
end
it "serializes json wrapped in json value" do
response = Panko::Response.new(success: true, value: Panko::JsonValue.from('{"a":1}'))
json_response = Oj.load(response.to_json)
expect(json_response["success"]).to eq(true)
expect(json_response["value"]).to eq("a" => 1)
end
it "serializes array serializer" do
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
response = Panko::Response.new(success: true,
foos: Panko::ArraySerializer.new(Foo.all, each_serializer: FooSerializer))
json_response = Oj.load(response.to_json)
expect(json_response["success"]).to eq(true)
expect(json_response["foos"]).to eq([
"name" => foo.name,
"address" => foo.address
])
end
it "supports nesting of responses" do
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
response = Panko::Response.new(
data: Panko::Response.new(
data: Panko::Response.new(
rows: [
Panko::Response.new(
foos: Panko::ArraySerializer.new(Foo.all, each_serializer: FooSerializer)
)
]
)
)
)
json_response = Oj.load(response.to_json)
expect(json_response).to eq(
"data" => {
"data" => {
"rows" => [
"foos" => [{
"name" => foo.name,
"address" => foo.address
}]
]
}
}
)
end
it "supports array" do
response = Panko::Response.new([
data: Panko::Response.new(
json_data: Panko::JsonValue.from({a: 1}.to_json)
)
])
json_response = Oj.load(response.to_json)
expect(json_response).to eql([
{"data" => {"json_data" => {"a" => 1}}}
])
end
it "create" do
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
response = Panko::Response.create do |t|
[
{
data: t.value(
json_data: t.json({a: 1}.to_json),
foos: t.array_serializer(Foo.all, FooSerializer),
foo: t.serializer(Foo.first, FooSerializer)
)
}
]
end
json_response = Oj.load(response.to_json)
expect(json_response).to eql([
{"data" =>
{
"json_data" => {"a" => 1},
"foo" => {
"name" => foo.name,
"address" => foo.address
},
"foos" => [{
"name" => foo.name,
"address" => foo.address
}]
}}
])
end
it "create with context" do
foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
context = {value: Faker::Lorem.word}
response = Panko::Response.create do |t|
[
{
data: t.value(
foos: t.array_serializer(Foo.all, FooWithContextSerializer, context: context),
foo: t.serializer(Foo.first, FooWithContextSerializer, context: context)
)
}
]
end
json_response = Oj.load(response.to_json)
expect(json_response).to eql([
{"data" =>
{
"foo" => {
"name" => foo.name,
"context_value" => context[:value]
},
"foos" => [{
"name" => foo.name,
"context_value" => context[:value]
}]
}}
])
end
end
================================================
FILE: spec/unit/panko/serialization_descriptor_spec.rb
================================================
# frozen_string_literal: true
require "spec_helper"
describe Panko::SerializationDescriptor do
class FooSerializer < Panko::Serializer
attributes :name, :address
end
context "attributes" do
it "simple fields" do
descriptor = Panko::SerializationDescriptor.build(FooSerializer)
expect(descriptor).not_to be_nil
expect(descriptor.attributes).to eq([
P
gitextract_w1z8tyjv/
├── .clang-format
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── database_matrix.yml
│ ├── docs.yml
│ ├── lint.yml
│ └── tests.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Appraisals
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── benchmarks/
│ ├── object_writer.rb
│ ├── panko_json.rb
│ ├── panko_object.rb
│ ├── plain_object.rb
│ ├── support/
│ │ ├── benchmark.rb
│ │ ├── datasets.rb
│ │ └── setup.rb
│ └── type_casts/
│ ├── generic.rb
│ ├── mysql.rb
│ ├── postgresql.rb
│ └── sqlite.rb
├── docs/
│ ├── CNAME
│ ├── Gemfile
│ ├── _config.yml
│ ├── associations.md
│ ├── attributes.md
│ ├── design-choices.md
│ ├── getting-started.md
│ ├── index.md
│ ├── performance.md
│ ├── reference.md
│ └── response-bag.md
├── 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.2.0.gemfile
│ ├── 8.0.0.gemfile
│ └── 8.1.0.gemfile
├── 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/
├── features/
│ ├── active_record_serialization_spec.rb
│ ├── array_serializer_spec.rb
│ ├── associations_spec.rb
│ ├── attributes_spec.rb
│ ├── context_and_scope_spec.rb
│ ├── filtering_spec.rb
│ ├── hash_serialization_spec.rb
│ └── poro_serialization_spec.rb
├── spec_helper.rb
├── support/
│ └── database_config.rb
└── unit/
├── panko/
│ ├── array_serializer_spec.rb
│ ├── object_writer_spec.rb
│ ├── response_spec.rb
│ ├── serialization_descriptor_spec.rb
│ └── serializer_spec.rb
├── serializer_resolver_spec.rb
└── type_cast_spec.rb
SYMBOL INDEX (324 symbols across 48 files)
FILE: benchmarks/panko_json.rb
class AuthorFastSerializer (line 5) | class AuthorFastSerializer < Panko::Serializer
class PostFastSerializer (line 9) | class PostFastSerializer < Panko::Serializer
class PostFastWithMethodCallSerializer (line 13) | class PostFastWithMethodCallSerializer < Panko::Serializer
method method_call (line 16) | def method_call
class PostFastWithJsonSerializer (line 21) | class PostFastWithJsonSerializer < Panko::Serializer
class PostWithHasOneFastSerializer (line 25) | class PostWithHasOneFastSerializer < Panko::Serializer
class AuthorWithHasManyFastSerializer (line 31) | class AuthorWithHasManyFastSerializer < Panko::Serializer
class PostWithAliasFastSerializer (line 37) | class PostWithAliasFastSerializer < Panko::Serializer
FILE: benchmarks/panko_object.rb
class AuthorFastSerializer (line 5) | class AuthorFastSerializer < Panko::Serializer
class PostFastSerializer (line 9) | class PostFastSerializer < Panko::Serializer
class PostWithHasOneFastSerializer (line 13) | class PostWithHasOneFastSerializer < Panko::Serializer
class PostWithAliasFastSerializer (line 19) | class PostWithAliasFastSerializer < Panko::Serializer
FILE: benchmarks/plain_object.rb
class PlainAuthorSerializer (line 5) | class PlainAuthorSerializer < Panko::Serializer
class PlainPostSerializer (line 9) | class PlainPostSerializer < Panko::Serializer
class PlainPostWithMethodCallSerializer (line 13) | class PlainPostWithMethodCallSerializer < Panko::Serializer
method method_call (line 16) | def method_call
class PlainPostWithHasOneSerializer (line 21) | class PlainPostWithHasOneSerializer < Panko::Serializer
FILE: benchmarks/support/benchmark.rb
class NoopWriter (line 38) | class NoopWriter
method push_value (line 48) | def push_value(value, key = nil)
method push_json (line 57) | def push_json(value, key = nil) # rubocop:disable Lint/UnusedMethodArg...
function print_header (line 80) | def print_header
function benchmark (line 110) | def benchmark(label, &block)
function benchmark_with_records (line 160) | def benchmark_with_records(label, type:, &block)
function run_cpu_profile (line 179) | def run_cpu_profile
FILE: benchmarks/support/datasets.rb
class PlainAuthor (line 15) | class PlainAuthor
class PlainPost (line 19) | class PlainPost
method author= (line 23) | def author=(author)
FILE: benchmarks/support/setup.rb
class Author (line 31) | class Author < ActiveRecord::Base
class Post (line 35) | class Post < ActiveRecord::Base
class PostWithAliasModel (line 39) | class PostWithAliasModel < ActiveRecord::Base
FILE: benchmarks/type_casts/generic.rb
function bench_type (line 9) | def bench_type(type_klass, from, to, label: type_klass.name)
FILE: benchmarks/type_casts/mysql.rb
function bench_type (line 18) | def bench_type(type_klass, from, to, label: type_klass.name)
function bench_type_with_instance (line 30) | def bench_type_with_instance(instance, from, to, label:)
FILE: benchmarks/type_casts/postgresql.rb
function bench_type (line 17) | def bench_type(type_klass, from, to, label: type_klass.name)
FILE: ext/panko_serializer/attributes_writer/active_record.c
type attributes (line 14) | struct attributes {
function init_context (line 37) | struct attributes init_context(VALUE obj) {
function VALUE (line 75) | VALUE _read_value_from_indexed_row(struct attributes attributes_ctx,
function VALUE (line 99) | VALUE read_attribute(struct attributes attributes_ctx, Attribute attribute,
function active_record_attributes_writer (line 148) | void active_record_attributes_writer(VALUE obj, VALUE attributes,
function init_active_record_attributes_writer (line 167) | void init_active_record_attributes_writer(VALUE mPanko) {
function panko_init_active_record (line 178) | void panko_init_active_record(VALUE mPanko) {
FILE: ext/panko_serializer/attributes_writer/attributes_writer.c
function VALUE (line 6) | VALUE init_types(VALUE v) {
function AttributesWriter (line 22) | AttributesWriter create_attributes_writer(VALUE object) {
function empty_write_attributes (line 45) | void empty_write_attributes(VALUE obj, VALUE attributes, EachAttributeFu...
function AttributesWriter (line 48) | AttributesWriter create_empty_attributes_writer() {
function init_attributes_writer (line 53) | void init_attributes_writer(VALUE mPanko) {
FILE: ext/panko_serializer/attributes_writer/attributes_writer.h
type ObjectType (line 10) | enum ObjectType {
type AttributesWriter (line 17) | typedef struct _AttributesWriter {
FILE: ext/panko_serializer/attributes_writer/common.c
function VALUE (line 3) | VALUE attr_name_for_serialization(Attribute attribute) {
FILE: ext/panko_serializer/attributes_writer/hash.c
function hash_attributes_writer (line 3) | void hash_attributes_writer(VALUE obj, VALUE attributes,
FILE: ext/panko_serializer/attributes_writer/plain.c
function plain_attributes_writer (line 3) | void plain_attributes_writer(VALUE obj, VALUE attributes,
FILE: ext/panko_serializer/attributes_writer/type_cast/time_conversion.c
function VALUE (line 13) | VALUE is_iso8601_time_string(const char* value) {
function append_region_str (line 28) | void append_region_str(const char* source, char** to, int regionBegin,
function is_iso_ar_iso_datetime_string_fast_case (line 36) | bool is_iso_ar_iso_datetime_string_fast_case(const char* value) {
function is_iso_ar_iso_datetime_string_slow_case (line 54) | bool is_iso_ar_iso_datetime_string_slow_case(const char* value) {
function VALUE (line 72) | VALUE iso_ar_iso_datetime_string(const char* value) {
function build_regex (line 130) | void build_regex(OnigRegex* reg, const UChar* pattern) {
function panko_init_time (line 144) | void panko_init_time(VALUE mPanko) {
FILE: ext/panko_serializer/attributes_writer/type_cast/type_cast.c
function VALUE (line 34) | VALUE cache_postgres_type_lookup(VALUE ar) {
function VALUE (line 85) | VALUE cache_time_zone_type_lookup(VALUE ar) {
function cache_type_lookup (line 112) | void cache_type_lookup() {
function is_string_or_text_type (line 157) | bool is_string_or_text_type(VALUE type_klass) {
function VALUE (line 162) | VALUE cast_string_or_text_type(VALUE value) {
function is_float_type (line 178) | bool is_float_type(VALUE type_klass) {
function VALUE (line 183) | VALUE cast_float_type(VALUE value) {
function is_integer_type (line 196) | bool is_integer_type(VALUE type_klass) {
function VALUE (line 201) | VALUE cast_integer_type(VALUE value) {
function is_json_type (line 233) | bool is_json_type(VALUE type_klass) {
function is_boolean_type (line 239) | bool is_boolean_type(VALUE type_klass) { return type_klass == ar_boolean...
function VALUE (line 241) | VALUE cast_boolean_type(VALUE value) {
function is_date_time_type (line 272) | bool is_date_time_type(VALUE type_klass) {
function VALUE (line 282) | VALUE cast_date_time_type(VALUE value) {
function VALUE (line 302) | VALUE rescue_func(VALUE _arg, VALUE _data) { return Qfalse; }
function VALUE (line 304) | VALUE parse_json(VALUE value) {
function VALUE (line 308) | VALUE is_json_value(VALUE value) {
function VALUE (line 332) | VALUE type_cast(VALUE type_metadata, VALUE value, volatile VALUE* isJson) {
function VALUE (line 367) | VALUE public_type_cast(int argc, VALUE* argv, VALUE self) {
function panko_init_type_cast (line 378) | void panko_init_type_cast(VALUE mPanko) {
FILE: ext/panko_serializer/attributes_writer/type_cast/type_cast.h
type VALUE (line 31) | typedef VALUE (*TypeCastFunc)(VALUE value);
type _TypeCast (line 33) | struct _TypeCast {
type _TypeCast (line 65) | struct _TypeCast
FILE: ext/panko_serializer/panko_serializer.c
function write_value (line 18) | void write_value(VALUE str_writer, VALUE key, VALUE value, VALUE isJson) {
function serialize_method_fields (line 26) | void serialize_method_fields(VALUE object, VALUE str_writer,
function serialize_fields (line 54) | void serialize_fields(VALUE object, VALUE str_writer,
function serialize_has_one_associations (line 62) | void serialize_has_one_associations(VALUE object, VALUE str_writer,
function serialize_has_many_associations (line 80) | void serialize_has_many_associations(VALUE object, VALUE str_writer,
function VALUE (line 98) | VALUE serialize_object(VALUE key, VALUE object, VALUE str_writer,
function VALUE (line 121) | VALUE serialize_objects(VALUE key, VALUE objects, VALUE str_writer,
function VALUE (line 141) | VALUE serialize_object_api(VALUE klass, VALUE object, VALUE str_writer,
function VALUE (line 147) | VALUE serialize_objects_api(VALUE klass, VALUE objects, VALUE str_writer,
function Init_panko_serializer (line 154) | void Init_panko_serializer() {
FILE: ext/panko_serializer/serialization_descriptor/association.c
function association_free (line 5) | static void association_free(void* ptr) {
function association_mark (line 23) | void association_mark(Association data) {
function VALUE (line 33) | static VALUE association_new(int argc, VALUE* argv, VALUE self) {
function Association (line 51) | Association association_read(VALUE association) {
function VALUE (line 55) | VALUE association_name_sym_ref(VALUE self) {
function VALUE (line 60) | VALUE association_name_str_ref(VALUE self) {
function VALUE (line 65) | VALUE association_descriptor_ref(VALUE self) {
function VALUE (line 70) | VALUE association_decriptor_aset(VALUE self, VALUE descriptor) {
function panko_init_association (line 79) | void panko_init_association(VALUE mPanko) {
FILE: ext/panko_serializer/serialization_descriptor/association.h
type _Association (line 8) | struct _Association {
FILE: ext/panko_serializer/serialization_descriptor/attribute.c
function attribute_free (line 6) | static void attribute_free(void* ptr) {
function attribute_mark (line 21) | void attribute_mark(Attribute data) {
function VALUE (line 28) | static VALUE attribute_new(int argc, VALUE* argv, VALUE self) {
function Attribute (line 47) | Attribute attribute_read(VALUE attribute) {
function attribute_try_invalidate (line 51) | void attribute_try_invalidate(Attribute attribute, VALUE new_record_clas...
function VALUE (line 75) | VALUE attribute_name_ref(VALUE self) {
function VALUE (line 80) | VALUE attribute_alias_name_ref(VALUE self) {
function panko_init_attribute (line 85) | void panko_init_attribute(VALUE mPanko) {
FILE: ext/panko_serializer/serialization_descriptor/attribute.h
type _Attribute (line 8) | struct _Attribute {
FILE: ext/panko_serializer/serialization_descriptor/serialization_descriptor.c
function sd_free (line 6) | static void sd_free(SerializationDescriptor sd) {
function sd_mark (line 21) | void sd_mark(SerializationDescriptor data) {
function VALUE (line 31) | static VALUE sd_alloc(VALUE klass) {
function SerializationDescriptor (line 47) | SerializationDescriptor sd_read(VALUE descriptor) {
function sd_set_writer (line 51) | void sd_set_writer(SerializationDescriptor sd, VALUE object) {
function VALUE (line 59) | VALUE sd_serializer_set(VALUE self, VALUE serializer) {
function VALUE (line 66) | VALUE sd_serializer_ref(VALUE self) {
function VALUE (line 72) | VALUE sd_attributes_set(VALUE self, VALUE attributes) {
function VALUE (line 79) | VALUE sd_attributes_ref(VALUE self) {
function VALUE (line 84) | VALUE sd_method_fields_set(VALUE self, VALUE method_fields) {
function VALUE (line 90) | VALUE sd_method_fields_ref(VALUE self) {
function VALUE (line 95) | VALUE sd_has_one_associations_set(VALUE self, VALUE has_one_associations) {
function VALUE (line 101) | VALUE sd_has_one_associations_ref(VALUE self) {
function VALUE (line 106) | VALUE sd_has_many_associations_set(VALUE self, VALUE has_many_associatio...
function VALUE (line 112) | VALUE sd_has_many_associations_ref(VALUE self) {
function VALUE (line 117) | VALUE sd_type_set(VALUE self, VALUE type) {
function VALUE (line 123) | VALUE sd_type_aref(VALUE self) {
function VALUE (line 128) | VALUE sd_aliases_set(VALUE self, VALUE aliases) {
function VALUE (line 134) | VALUE sd_aliases_aref(VALUE self) {
function panko_init_serialization_descriptor (line 139) | void panko_init_serialization_descriptor(VALUE mPanko) {
FILE: ext/panko_serializer/serialization_descriptor/serialization_descriptor.h
type _SerializationDescriptor (line 8) | struct _SerializationDescriptor {
FILE: lib/panko/array_serializer.rb
type Panko (line 3) | module Panko
class ArraySerializer (line 4) | class ArraySerializer
method initialize (line 7) | def initialize(subjects, options = {})
method to_json (line 29) | def to_json
method serialize (line 33) | def serialize(subjects)
method to_a (line 37) | def to_a
method serialize_to_json (line 41) | def serialize_to_json(subjects)
method serialize_with_writer (line 47) | def serialize_with_writer(subjects, writer)
FILE: lib/panko/association.rb
type Panko (line 3) | module Panko
class Association (line 4) | class Association
method duplicate (line 5) | def duplicate
method inspect (line 13) | def inspect
FILE: lib/panko/attribute.rb
type Panko (line 3) | module Panko
class Attribute (line 4) | class Attribute
method create (line 5) | def self.create(name, alias_name: nil)
method == (line 10) | def ==(other)
method hash (line 17) | def hash
method eql? (line 21) | def eql?(other)
method inspect (line 25) | def inspect
FILE: lib/panko/object_writer.rb
class Panko::ObjectWriter (line 3) | class Panko::ObjectWriter
method initialize (line 4) | def initialize
method push_object (line 12) | def push_object(key = nil)
method push_array (line 17) | def push_array(key = nil)
method push_key (line 22) | def push_key(key)
method push_value (line 26) | def push_value(value, key = nil)
method push_json (line 36) | def push_json(value, key = nil)
method pop (line 48) | def pop
method output (line 64) | def output
FILE: lib/panko/response.rb
type Panko (line 5) | module Panko
function from (line 7) | def self.from(value)
function to_json (line 11) | def to_json
class ResponseCreator (line 16) | class ResponseCreator
method value (line 17) | def self.value(value)
method json (line 21) | def self.json(value)
method array_serializer (line 25) | def self.array_serializer(data, serializer, options = {})
method serializer (line 30) | def self.serializer(data, serializer, options = {})
class Response (line 35) | class Response
method initialize (line 36) | def initialize(data)
method to_json (line 40) | def to_json(_options = nil)
method create (line 46) | def self.create
method write (line 52) | def write(writer, data, key = nil)
method write_array (line 60) | def write_array(writer, value, key = nil)
method write_object (line 66) | def write_object(writer, value, key = nil)
method write_value (line 76) | def write_value(writer, value, key = nil)
FILE: lib/panko/serialization_descriptor.rb
type Panko (line 3) | module Panko
class SerializationDescriptor (line 4) | class SerializationDescriptor
method build (line 9) | def self.build(serializer, options = {}, serialization_context = nil)
method duplicate (line 25) | def self.duplicate(descriptor)
method set_serialization_context (line 41) | def set_serialization_context(context)
method apply_filters (line 56) | def apply_filters(options)
method apply_association_filters (line 91) | def apply_association_filters(associations, only_filters, except_fil...
method resolve_filters (line 134) | def resolve_filters(options, filter)
method apply_fields_filters (line 151) | def apply_fields_filters(fields, only, except)
method apply_attribute_filters (line 158) | def apply_attribute_filters(attributes, only, except)
FILE: lib/panko/serializer.rb
class SerializationContext (line 6) | class SerializationContext
method initialize (line 9) | def initialize(context, scope)
method create (line 14) | def self.create(options)
class EmptySerializerContext (line 23) | class EmptySerializerContext
method scope (line 24) | def scope
method context (line 28) | def context
type Panko (line 33) | module Panko
class Serializer (line 34) | class Serializer
method inherited (line 38) | def inherited(base)
method attributes (line 57) | def attributes(*attrs)
method aliases (line 61) | def aliases(aliases = {})
method method_added (line 67) | def method_added(method)
method has_one (line 76) | def has_one(name, options = {})
method has_many (line 92) | def has_many(name, options = {})
method initialize (line 109) | def initialize(options = {})
method context (line 118) | def context
method scope (line 122) | def scope
method serialize (line 129) | def serialize(object)
method serialize_to_json (line 133) | def serialize_to_json(object)
method serialize_with_writer (line 139) | def serialize_with_writer(object, writer)
FILE: lib/panko/serializer_resolver.rb
class Panko::SerializerResolver (line 6) | class Panko::SerializerResolver
method resolve (line 8) | def resolve(name, from)
method namespace_for (line 25) | def namespace_for(from)
method namespace_for (line 29) | def namespace_for(from)
method safe_serializer_get (line 34) | def safe_serializer_get(name)
FILE: lib/panko/version.rb
type Panko (line 3) | module Panko
FILE: spec/features/active_record_serialization_spec.rb
class FooSerializer (line 16) | class FooSerializer < Panko::Serializer
class FooSerializer (line 28) | class FooSerializer < Panko::Serializer
class FooSerializer (line 40) | class FooSerializer < Panko::Serializer
FILE: spec/features/array_serializer_spec.rb
class TestSerializerWithMethodsSerializer (line 43) | class TestSerializerWithMethodsSerializer < Panko::Serializer
method something (line 46) | def something
method context_fetch (line 50) | def context_fetch
FILE: spec/features/associations_spec.rb
class PlainFooHolder (line 34) | class PlainFooHolder
method initialize (line 37) | def initialize(name, foo)
class PlainFoo (line 43) | class PlainFoo
method initialize (line 46) | def initialize(name, address)
class FooSerializer (line 52) | class FooSerializer < Panko::Serializer
class PlainFooHolderHasOneSerializer (line 56) | class PlainFooHolderHasOneSerializer < Panko::Serializer
class FooHolderHasOneWithStringSerializer (line 73) | class FooHolderHasOneWithStringSerializer < Panko::Serializer
class FooHolderHasOneWithNameSerializer (line 90) | class FooHolderHasOneWithNameSerializer < Panko::Serializer
class FooHolderHasOneSerializer (line 107) | class FooHolderHasOneSerializer < Panko::Serializer
class FooHolderHasOneSerializer (line 124) | class FooHolderHasOneSerializer < Panko::Serializer
class NotFoundHasOneSerializer (line 142) | class NotFoundHasOneSerializer < Panko::Serializer
class VirtualSerializer (line 151) | class VirtualSerializer < Panko::Serializer
method virtual (line 154) | def virtual
class FooHolderHasOneVirtualSerializer (line 159) | class FooHolderHasOneVirtualSerializer < Panko::Serializer
class FooHolderHasOneSerializer (line 175) | class FooHolderHasOneSerializer < Panko::Serializer
class FooSerializer (line 206) | class FooSerializer < Panko::Serializer
class FooHolderHasOnePooWithStringSerializer (line 210) | class FooHolderHasOnePooWithStringSerializer < Panko::Serializer
class FoosHasManyHolderSerializer (line 260) | class FoosHasManyHolderSerializer < Panko::Serializer
class FoosHasManyHolderSerializer (line 284) | class FoosHasManyHolderSerializer < Panko::Serializer
class FoosHasManyHolderWithNameSerializer (line 308) | class FoosHasManyHolderWithNameSerializer < Panko::Serializer
class FoosHasManyHolderSerializer (line 332) | class FoosHasManyHolderSerializer < Panko::Serializer
class NotFoundHasManySerializer (line 357) | class NotFoundHasManySerializer < Panko::Serializer
class FoosHasManyHolderSerializer (line 366) | class FoosHasManyHolderSerializer < Panko::Serializer
class FoosHolderWithOnlySerializer (line 390) | class FoosHolderWithOnlySerializer < Panko::Serializer
class FooSerializer (line 432) | class FooSerializer < Panko::Serializer
class FoosHasManyPoosHolderSerializer (line 436) | class FoosHasManyPoosHolderSerializer < Panko::Serializer
class CommentSerializer (line 483) | class CommentSerializer < Panko::Serializer
class PostSerializer (line 487) | class PostSerializer < Panko::Serializer
class CommentSerializer (line 524) | class CommentSerializer < Panko::Serializer
class ArticleSerializer (line 528) | class ArticleSerializer < Panko::Serializer
class UserSerializer (line 576) | class UserSerializer < Panko::Serializer
class TeamSerializer (line 580) | class TeamSerializer < Panko::Serializer
class OrganizationSerializer (line 585) | class OrganizationSerializer < Panko::Serializer
class FoosHolderCombinedOptionsSerializer (line 660) | class FoosHolderCombinedOptionsSerializer < Panko::Serializer
class FooHolderCombinedOptionsSerializer (line 679) | class FooHolderCombinedOptionsSerializer < Panko::Serializer
class FooHolderNilSerializer (line 736) | class FooHolderNilSerializer < Panko::Serializer
class FoosHolderEmptySerializer (line 750) | class FoosHolderEmptySerializer < Panko::Serializer
class FlexibleSerializer (line 785) | class FlexibleSerializer < Panko::Serializer
method name (line 788) | def name
method address (line 793) | def address
class FooHolderFlexibleSerializer (line 798) | class FooHolderFlexibleSerializer < Panko::Serializer
FILE: spec/features/attributes_spec.rb
class FooSerializer (line 15) | class FooSerializer < Panko::Serializer
class FooWithMethodsSerializer (line 36) | class FooWithMethodsSerializer < Panko::Serializer
method something (line 39) | def something
method another_method (line 43) | def another_method
class BaseSerializer (line 65) | class BaseSerializer < Panko::Serializer
class ChildSerializer (line 69) | class ChildSerializer < BaseSerializer
class ObjectWithTimeSerializer (line 90) | class ObjectWithTimeSerializer < Panko::Serializer
method method (line 93) | def method
class FooValueSerializer (line 116) | class FooValueSerializer < Panko::Serializer
class FooWithAliasesModel (line 138) | class FooWithAliasesModel < ActiveRecord::Base
class FooWithArAliasesSerializer (line 143) | class FooWithArAliasesSerializer < Panko::Serializer
class FooWithAliasesSerializer (line 153) | class FooWithAliasesSerializer < Panko::Serializer
function created_at (line 186) | def created_at
class FooSerializer (line 209) | class FooSerializer < Panko::Serializer
class FooSkipSerializer (line 228) | class FooSkipSerializer < FooSerializer
method address (line 229) | def address
class FooSerializer (line 253) | class FooSerializer < Panko::Serializer
FILE: spec/features/context_and_scope_spec.rb
class FooWithContextSerializer (line 17) | class FooWithContextSerializer < Panko::Serializer
method context_value (line 20) | def context_value
function scope_value (line 58) | def scope_value
function scope_value (line 70) | def scope_value
FILE: spec/features/filtering_spec.rb
class FoosHolderForFilterTestSerializer (line 67) | class FoosHolderForFilterTestSerializer < Panko::Serializer
class FoosHolderForFilterTestSerializer (line 92) | class FoosHolderForFilterTestSerializer < Panko::Serializer
class TeamWithOrgSerializer (line 194) | class TeamWithOrgSerializer < Panko::Serializer
class FooWithAliasFilterSerializer (line 223) | class FooWithAliasFilterSerializer < Panko::Serializer
class TeamArrayFilterSerializer (line 271) | class TeamArrayFilterSerializer < Panko::Serializer
class TeamWithExceptSerializer (line 328) | class TeamWithExceptSerializer < Panko::Serializer
class FooWithFiltersForSerializer (line 382) | class FooWithFiltersForSerializer < Panko::Serializer
method filters_for (line 385) | def self.filters_for(context, scope)
method filters_for (line 584) | def self.filters_for(context, scope)
class FooWithContextFiltersSerializer (line 397) | class FooWithContextFiltersSerializer < Panko::Serializer
method filters_for (line 400) | def self.filters_for(context, scope)
class FooWithFiltersForSerializer (line 581) | class FooWithFiltersForSerializer < Panko::Serializer
method filters_for (line 385) | def self.filters_for(context, scope)
method filters_for (line 584) | def self.filters_for(context, scope)
FILE: spec/features/hash_serialization_spec.rb
class FooSerializer (line 6) | class FooSerializer < Panko::Serializer
FILE: spec/features/poro_serialization_spec.rb
class FooSerializer (line 6) | class FooSerializer < Panko::Serializer
class PlainFoo (line 11) | class PlainFoo
method initialize (line 14) | def initialize(name, address)
FILE: spec/support/database_config.rb
class DatabaseConfig (line 4) | class DatabaseConfig
method database_type (line 28) | def self.database_type
method config (line 32) | def self.config
method setup_database (line 39) | def self.setup_database
method teardown_database (line 46) | def self.teardown_database
FILE: spec/unit/panko/response_spec.rb
function context_value (line 25) | def context_value
FILE: spec/unit/panko/serialization_descriptor_spec.rb
class FooSerializer (line 6) | class FooSerializer < Panko::Serializer
class SerializerWithMethodsSerializer (line 22) | class SerializerWithMethodsSerializer < Panko::Serializer
method something (line 25) | def something
class AttribteAliasesSerializer (line 41) | class AttribteAliasesSerializer < Panko::Serializer
class MultipleFiltersTestSerializer (line 54) | class MultipleFiltersTestSerializer < Panko::Serializer
class BuilderTestFooHolderHasOneSerializer (line 79) | class BuilderTestFooHolderHasOneSerializer < Panko::Serializer
class BuilderTestFoosHasManyHolderSerializer (line 102) | class BuilderTestFoosHasManyHolderSerializer < Panko::Serializer
class ExceptFooWithAliasesSerializer (line 144) | class ExceptFooWithAliasesSerializer < Panko::Serializer
class OnlyFooWithAliasesSerializer (line 155) | class OnlyFooWithAliasesSerializer < Panko::Serializer
class OnlyWithFieldsFooWithAliasesSerializer (line 167) | class OnlyWithFieldsFooWithAliasesSerializer < Panko::Serializer
class FooHasOneSerilizers (line 182) | class FooHasOneSerilizers < Panko::Serializer
class AssocFilterTestFoosHolderSerializer (line 201) | class AssocFilterTestFoosHolderSerializer < Panko::Serializer
FILE: spec/unit/panko/serializer_spec.rb
function computed_field (line 80) | def computed_field
function computed_field (line 97) | def computed_field
FILE: spec/unit/serializer_resolver_spec.rb
class CoolSerializer (line 7) | class CoolSerializer < Panko::Serializer
class PersonSerializer (line 17) | class PersonSerializer < Panko::Serializer
class MyCoolSerializer (line 27) | class MyCoolSerializer < Panko::Serializer
class CoolSerializer (line 37) | class CoolSerializer < Panko::Serializer
type MyApp (line 40) | module MyApp
class CoolSerializer (line 41) | class CoolSerializer < Panko::Serializer
class PersonSerializer (line 44) | class PersonSerializer < Panko::Serializer
class SomeObjectSerializer (line 63) | class SomeObjectSerializer
FILE: spec/unit/type_cast_spec.rb
function check_if_exists (line 6) | def check_if_exists(module_name)
Condensed preview — 91 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (225K chars).
[
{
"path": ".clang-format",
"chars": 47,
"preview": "---\nLanguage: Cpp\nBasedOnStyle: Google\n"
},
{
"path": ".github/dependabot.yml",
"chars": 460,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"github-actions\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n\n "
},
{
"path": ".github/workflows/database_matrix.yml",
"chars": 2404,
"preview": "name: Database Tests\n\non: [push, pull_request]\n\njobs:\n database-matrix:\n runs-on: ubuntu-latest\n \n strategy:\n "
},
{
"path": ".github/workflows/docs.yml",
"chars": 1167,
"preview": "name: Docs Publishing\n\non:\n push:\n branches: [master]\n\n workflow_dispatch:\n\npermissions:\n contents: read\n pages: "
},
{
"path": ".github/workflows/lint.yml",
"chars": 629,
"preview": "name: Lint\n\non: [push, pull_request]\n\njobs:\n lint:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkou"
},
{
"path": ".github/workflows/tests.yml",
"chars": 815,
"preview": "name: Panko Serializer CI\n\non: [push, pull_request]\n\njobs:\n tests:\n runs-on: ubuntu-latest\n strategy:\n fail-"
},
{
"path": ".gitignore",
"chars": 227,
"preview": "/.bundle/\n/.yardoc\n/Gemfile.lock\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n/vendor/bundle/\n.byebug_history\n*"
},
{
"path": ".rspec",
"chars": 31,
"preview": "--format documentation\n--color\n"
},
{
"path": ".rubocop.yml",
"chars": 652,
"preview": "# We want Exclude directives from different\n# config files to get merged, not overwritten\ninherit_mode:\n merge:\n - E"
},
{
"path": "Appraisals",
"chars": 573,
"preview": "# frozen_string_literal: true\n\nappraise \"7.2.0\" do\n gem \"activesupport\", \"~> 7.2.0\"\n gem \"activemodel\", \"~> 7.2.0\"\n g"
},
{
"path": "Gemfile",
"chars": 486,
"preview": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\ngemspec\n\ngroup :benchmarks do\n gem \"vernier\"\n gem \"stack"
},
{
"path": "LICENSE.txt",
"chars": 1078,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2017 Yosi Attias\n\nPermission is hereby granted, free of charge, to any person obtai"
},
{
"path": "README.md",
"chars": 979,
"preview": "# Panko\n\n {\n volatile VALUE name_str = attribute->nam"
},
{
"path": "ext/panko_serializer/attributes_writer/common.h",
"chars": 263,
"preview": "#pragma once\n\n#include \"../serialization_descriptor/attribute.h\"\n#include \"ruby.h\"\n\ntypedef void (*EachAttributeFunc)(VA"
},
{
"path": "ext/panko_serializer/attributes_writer/hash.c",
"chars": 466,
"preview": "#include \"hash.h\"\n\nvoid hash_attributes_writer(VALUE obj, VALUE attributes,\n EachAttributeFun"
},
{
"path": "ext/panko_serializer/attributes_writer/hash.h",
"chars": 177,
"preview": "#pragma once\n\n#include \"common.h\"\n#include \"ruby.h\"\n\nvoid hash_attributes_writer(VALUE obj, VALUE attributes, EachAttrib"
},
{
"path": "ext/panko_serializer/attributes_writer/plain.c",
"chars": 469,
"preview": "#include \"plain.h\"\n\nvoid plain_attributes_writer(VALUE obj, VALUE attributes,\n EachAttribute"
},
{
"path": "ext/panko_serializer/attributes_writer/plain.h",
"chars": 179,
"preview": "#pragma once\n\n#include \"common.h\"\n#include \"ruby.h\"\n\nvoid plain_attributes_writer(VALUE obj, VALUE attributes,\n "
},
{
"path": "ext/panko_serializer/attributes_writer/type_cast/time_conversion.c",
"chars": 4205,
"preview": "#include \"time_conversion.h\"\n\nconst int YEAR_REGION = 1;\nconst int MONTH_REGION = 2;\nconst int DAY_REGION = 3;\nconst int"
},
{
"path": "ext/panko_serializer/attributes_writer/type_cast/time_conversion.h",
"chars": 239,
"preview": "#pragma once\n\n#include <ctype.h>\n#include <ruby.h>\n#include <ruby/oniguruma.h>\n#include <stdbool.h>\n\nVALUE is_iso8601_ti"
},
{
"path": "ext/panko_serializer/attributes_writer/type_cast/type_cast.c",
"chars": 11333,
"preview": "#include \"type_cast.h\"\n\n#include \"time_conversion.h\"\n\nID deserialize_from_db_id = 0;\nID to_s_id = 0;\nID to_i_id = 0;\n\nst"
},
{
"path": "ext/panko_serializer/attributes_writer/type_cast/type_cast.h",
"chars": 2406,
"preview": "#pragma once\n\n#include <ruby.h>\n#include <stdbool.h>\n\n/*\n * Type Casting\n *\n * We do \"special\" type casting which is mix"
},
{
"path": "ext/panko_serializer/common.h",
"chars": 227,
"preview": "#pragma once\n\n#include <ruby.h>\n\n#define PANKO_SAFE_HASH_SIZE(hash) \\\n (hash == Qnil || hash == Qundef) ? 0 : RHASH_SIZ"
},
{
"path": "ext/panko_serializer/extconf.rb",
"chars": 774,
"preview": "# frozen_string_literal: true\nrequire \"mkmf\"\nrequire \"pathname\"\n\n$CPPFLAGS += \" -Wall\"\n\nextension_name = \"panko_serializ"
},
{
"path": "ext/panko_serializer/panko_serializer.c",
"chars": 5559,
"preview": "#include \"panko_serializer.h\"\n\n#include <ruby.h>\n\nstatic ID push_value_id;\nstatic ID push_array_id;\nstatic ID push_objec"
},
{
"path": "ext/panko_serializer/panko_serializer.h",
"chars": 486,
"preview": "#include <ruby.h>\n\n#include \"attributes_writer/attributes_writer.h\"\n#include \"serialization_descriptor/association.h\"\n#i"
},
{
"path": "ext/panko_serializer/serialization_descriptor/association.c",
"chars": 2566,
"preview": "#include \"association.h\"\n\nVALUE cAssociation;\n\nstatic void association_free(void* ptr) {\n if (!ptr) {\n return;\n }\n\n"
},
{
"path": "ext/panko_serializer/serialization_descriptor/association.h",
"chars": 370,
"preview": "#include <ruby.h>\n\n#ifndef __ASSOCIATION_H__\n#define __ASSOCIATION_H__\n\n#include \"serialization_descriptor.h\"\n\ntypedef s"
},
{
"path": "ext/panko_serializer/serialization_descriptor/attribute.c",
"chars": 2739,
"preview": "#include \"attribute.h\"\n\nID attribute_aliases_id = 0;\nVALUE cAttribute;\n\nstatic void attribute_free(void* ptr) {\n if (!p"
},
{
"path": "ext/panko_serializer/serialization_descriptor/attribute.h",
"chars": 542,
"preview": "#include <ruby.h>\n\n#ifndef __ATTRIBUTE_H__\n#define __ATTRIBUTE_H__\n\n#include \"../common.h\"\n\ntypedef struct _Attribute {\n"
},
{
"path": "ext/panko_serializer/serialization_descriptor/serialization_descriptor.c",
"chars": 5453,
"preview": "#include \"serialization_descriptor.h\"\n\nstatic ID object_id;\nstatic ID sc_id;\n\nstatic void sd_free(SerializationDescripto"
},
{
"path": "ext/panko_serializer/serialization_descriptor/serialization_descriptor.h",
"chars": 702,
"preview": "#pragma once\n\n#include <ruby.h>\n#include <stdbool.h>\n\n#include \"attributes_writer/attributes_writer.h\"\n\ntypedef struct _"
},
{
"path": "gemfiles/7.2.0.gemfile",
"chars": 651,
"preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activesupport\", \"~> 7.2.0\"\ngem \"activemodel\""
},
{
"path": "gemfiles/8.0.0.gemfile",
"chars": 651,
"preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activesupport\", \"~> 8.0.0\"\ngem \"activemodel\""
},
{
"path": "gemfiles/8.1.0.gemfile",
"chars": 651,
"preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activesupport\", \"~> 8.1.0\"\ngem \"activemodel\""
},
{
"path": "lib/panko/array_serializer.rb",
"chars": 1365,
"preview": "# frozen_string_literal: true\n\nmodule Panko\n class ArraySerializer\n attr_accessor :subjects\n\n def initialize(subj"
},
{
"path": "lib/panko/association.rb",
"chars": 314,
"preview": "# frozen_string_literal: true\n\nmodule Panko\n class Association\n def duplicate\n Panko::Association.new(\n "
},
{
"path": "lib/panko/attribute.rb",
"chars": 619,
"preview": "# frozen_string_literal: true\n\nmodule Panko\n class Attribute\n def self.create(name, alias_name: nil)\n alias_nam"
},
{
"path": "lib/panko/object_writer.rb",
"chars": 1113,
"preview": "# frozen_string_literal: true\n\nclass Panko::ObjectWriter\n def initialize\n @values = []\n @keys = []\n\n @next_key"
},
{
"path": "lib/panko/response.rb",
"chars": 1882,
"preview": "# frozen_string_literal: true\n\nrequire \"oj\"\n\nmodule Panko\n JsonValue = Struct.new(:value) do\n def self.from(value)\n "
},
{
"path": "lib/panko/serialization_descriptor.rb",
"chars": 5698,
"preview": "# frozen_string_literal: true\n\nmodule Panko\n class SerializationDescriptor\n #\n # Creates new description and appl"
},
{
"path": "lib/panko/serializer.rb",
"chars": 4106,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"serialization_descriptor\"\nrequire \"oj\"\n\nclass SerializationContext\n at"
},
{
"path": "lib/panko/serializer_resolver.rb",
"chars": 988,
"preview": "# frozen_string_literal: true\n\nrequire \"active_support/core_ext/string/inflections\"\nrequire \"active_support/core_ext/mod"
},
{
"path": "lib/panko/version.rb",
"chars": 68,
"preview": "# frozen_string_literal: true\n\nmodule Panko\n VERSION = \"0.8.5\"\nend\n"
},
{
"path": "lib/panko_serializer.rb",
"chars": 321,
"preview": "# frozen_string_literal: true\n\nrequire \"panko/version\"\nrequire \"panko/attribute\"\nrequire \"panko/association\"\nrequire \"pa"
},
{
"path": "panko_serializer.gemspec",
"chars": 1135,
"preview": "# frozen_string_literal: true\n\nlib = File.expand_path(\"lib\", __dir__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?"
},
{
"path": "spec/features/active_record_serialization_spec.rb",
"chars": 1254,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"ActiveRecord Serialization\" do\n before do\n Temping.c"
},
{
"path": "spec/features/array_serializer_spec.rb",
"chars": 3022,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe Panko::ArraySerializer do\n before do\n Temping.create("
},
{
"path": "spec/features/associations_spec.rb",
"chars": 22529,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"Associations Serialization\" do\n context \"has_one\" do\n "
},
{
"path": "spec/features/attributes_spec.rb",
"chars": 6651,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"Attributes Serialization\" do\n context \"instance variabl"
},
{
"path": "spec/features/context_and_scope_spec.rb",
"chars": 2616,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"Context and Scope\" do\n context \"context\" do\n before "
},
{
"path": "spec/features/filtering_spec.rb",
"chars": 16299,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"Filtering Serialization\" do\n context \"basic filtering\" "
},
{
"path": "spec/features/hash_serialization_spec.rb",
"chars": 1051,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"Hash Serialization\" do\n class FooSerializer < Panko::Se"
},
{
"path": "spec/features/poro_serialization_spec.rb",
"chars": 539,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"PORO Serialization\" do\n class FooSerializer < Panko::Se"
},
{
"path": "spec/spec_helper.rb",
"chars": 2159,
"preview": "# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nrequire \"logger\"\nrequire \"panko_serializer\"\nrequire \"faker\"\nrequi"
},
{
"path": "spec/support/database_config.rb",
"chars": 1533,
"preview": "# frozen_string_literal: true\n\n# Database configuration helper for tests\nclass DatabaseConfig\n ADAPTERS = {\n \"sqlite"
},
{
"path": "spec/unit/panko/array_serializer_spec.rb",
"chars": 5574,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe Panko::ArraySerializer do\n describe \"#initialize\" do\n "
},
{
"path": "spec/unit/panko/object_writer_spec.rb",
"chars": 1865,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\nrequire \"active_record/connection_adapters/postgresql_adapter\"\n\ndes"
},
{
"path": "spec/unit/panko/response_spec.rb",
"chars": 4493,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe Panko::Response do\n before do\n Temping.create(:foo) d"
},
{
"path": "spec/unit/panko/serialization_descriptor_spec.rb",
"chars": 7485,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe Panko::SerializationDescriptor do\n class FooSerializer <"
},
{
"path": "spec/unit/panko/serializer_spec.rb",
"chars": 6309,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe Panko::Serializer do\n describe \"class methods\" do\n de"
},
{
"path": "spec/unit/serializer_resolver_spec.rb",
"chars": 2074,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe Panko::SerializerResolver do\n it \"resolves serializer on"
},
{
"path": "spec/unit/type_cast_spec.rb",
"chars": 6073,
"preview": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\nrequire \"active_record/connection_adapters/postgresql_adapter\"\n\ndef"
}
]
About this extraction
This page contains the full source code of the yosiat/panko_serializer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 91 files (204.4 KB), approximately 54.3k tokens, and a symbol index with 324 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.