Full Code of yosiat/panko_serializer for AI

master 552ee5c99e29 cached
91 files
204.4 KB
54.3k tokens
324 symbols
1 requests
Download .txt
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

![Build Status](https://github.com/yosiat/panko_serializer/workflows/Panko%20Serializer%20CI/badge.svg?branch=master)

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&amp;repo=panko_serializer&amp;type=star&amp;count=true&amp;size=medium"
  frameborder="0" scrolling="0" width="150" height="20" title="GitHub Stars"></iframe>
  <br>
  Copyright &copy; 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
Download .txt
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
Download .txt
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![Build Status](https://github.com/yosiat/panko_serializer/workflows/Panko%20Serializer%20CI/badge.svg?branch=m"
  },
  {
    "path": "Rakefile",
    "chars": 1197,
    "preview": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rspec/core/rake_task\"\nrequire \"rubocop/rake_task\"\nre"
  },
  {
    "path": "benchmarks/object_writer.rb",
    "chars": 1141,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"support/benchmark\"\nrequire \"panko_serializer\"\n\nbenchmark(\"1 property, p"
  },
  {
    "path": "benchmarks/panko_json.rb",
    "chars": 2174,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"support/datasets\"\n\nclass AuthorFastSerializer < Panko::Serializer\n  att"
  },
  {
    "path": "benchmarks/panko_object.rb",
    "chars": 1297,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"support/datasets\"\n\nclass AuthorFastSerializer < Panko::Serializer\n  att"
  },
  {
    "path": "benchmarks/plain_object.rb",
    "chars": 1381,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"support/datasets\"\n\nclass PlainAuthorSerializer < Panko::Serializer\n  at"
  },
  {
    "path": "benchmarks/support/benchmark.rb",
    "chars": 6091,
    "preview": "# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nrequire \"benchmark/ips\"\nrequire \"memory_profiler\"\nrequire \"active"
  },
  {
    "path": "benchmarks/support/datasets.rb",
    "chars": 1060,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"benchmark\"\nrequire_relative \"setup\"\nrequire \"panko_serializer\"\n\n# --- A"
  },
  {
    "path": "benchmarks/support/setup.rb",
    "chars": 1254,
    "preview": "# frozen_string_literal: true\n\nrequire \"active_record\"\nrequire \"sqlite3\"\nrequire \"securerandom\"\n\n# Change the following "
  },
  {
    "path": "benchmarks/type_casts/generic.rb",
    "chars": 1329,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"../support/benchmark\"\nrequire \"active_record\"\nrequire \"panko_serializer"
  },
  {
    "path": "benchmarks/type_casts/mysql.rb",
    "chars": 2886,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"../support/benchmark\"\nrequire \"active_record\"\nrequire \"panko_serializer"
  },
  {
    "path": "benchmarks/type_casts/postgresql.rb",
    "chars": 1981,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"../support/benchmark\"\nrequire \"active_record\"\nrequire \"panko_serializer"
  },
  {
    "path": "benchmarks/type_casts/sqlite.rb",
    "chars": 478,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"../support/benchmark\"\nrequire \"active_record\"\nrequire \"sqlite3\"\nrequire"
  },
  {
    "path": "docs/CNAME",
    "chars": 10,
    "preview": "panko.dev\n"
  },
  {
    "path": "docs/Gemfile",
    "chars": 82,
    "preview": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\ngem \"just-the-docs\"\n"
  },
  {
    "path": "docs/_config.yml",
    "chars": 734,
    "preview": "title: Panko Serializers\ndescription: High Performance JSON Serialization for ActiveRecord & Ruby Objects\nurl: https://p"
  },
  {
    "path": "docs/associations.md",
    "chars": 3107,
    "preview": "---\ntitle: Associations\nlayout: default\nnav_order: 6\nparent: Reference\n---\n\n# Associations\n\nA serializer can define it's"
  },
  {
    "path": "docs/attributes.md",
    "chars": 3634,
    "preview": "---\ntitle: Attributes\nlayout: default\nnav_order: 5\nparent: Reference\n---\n\n# Attributes\n\nAttributes allow you to specify "
  },
  {
    "path": "docs/design-choices.md",
    "chars": 6205,
    "preview": "---\ntitle: Design Choices\nlayout: default\nnav_order: 4\n---\n\n# Design Choices\n\nIn short, Panko is a serializer for Active"
  },
  {
    "path": "docs/getting-started.md",
    "chars": 1308,
    "preview": "---\ntitle: Getting Started\nlayout: default\nnav_order: 2\n---\n\n# Getting Started\n\n## Installation\n\nTo install Panko, all y"
  },
  {
    "path": "docs/index.md",
    "chars": 582,
    "preview": "---\ntitle: Introduction\nlayout: default\nnav_order: 1\n---\n\n# Introduction\n\nPanko is a library which is inspired by Active"
  },
  {
    "path": "docs/performance.md",
    "chars": 1245,
    "preview": "---\ntitle: Performance\nlayout: default\nnav_order: 3\n---\n\n# Performance\n\nThe performance of Panko is measured using micro"
  },
  {
    "path": "docs/reference.md",
    "chars": 153,
    "preview": "---\ntitle: Reference\nlayout: default\nnav_order: 5\nhas_children: true\n---\n\n# Reference\n\nDetailed reference documentation "
  },
  {
    "path": "docs/response-bag.md",
    "chars": 1746,
    "preview": "---\ntitle: Response\nlayout: default\nnav_order: 7\nparent: Reference\n---\n\n# Response\n\nLet's say you have some JSON payload"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/active_record.c",
    "chars": 5335,
    "preview": "#include \"active_record.h\"\n\nstatic ID attributes_id;\nstatic ID types_id;\nstatic ID additional_types_id;\nstatic ID values"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/active_record.h",
    "chars": 485,
    "preview": "#pragma once\n\n#include <ruby.h>\n#include <stdbool.h>\n\n#include \"../common.h\"\n#include \"common.h\"\n#include \"serialization"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/attributes_writer.c",
    "chars": 1625,
    "preview": "#include \"attributes_writer.h\"\n\nstatic bool types_initialized = false;\nstatic VALUE ar_base_type = Qundef;\n\nVALUE init_t"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/attributes_writer.h",
    "chars": 737,
    "preview": "#pragma once\n\n#include <ruby.h>\n\n#include \"active_record.h\"\n#include \"common.h\"\n#include \"hash.h\"\n#include \"plain.h\"\n\nen"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/common.c",
    "chars": 230,
    "preview": "#include \"common.h\"\n\nVALUE attr_name_for_serialization(Attribute attribute) {\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.

Copied to clipboard!