[
  {
    "path": ".clang-format",
    "content": "---\nLanguage:        Cpp\nBasedOnStyle:  Google\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n\n  - package-ecosystem: \"bundler\"\n    directory: \"/docs\"\n    schedule:\n      interval: \"weekly\"\n    groups:\n      docs-dependencies:\n        patterns:\n          - \"*\"\n\n  - package-ecosystem: \"bundler\"\n    directory: \"/gemfiles\"\n    schedule:\n      interval: \"weekly\"\n    groups:\n      gemfiles-dependencies:\n        patterns:\n          - \"*\"\n"
  },
  {
    "path": ".github/workflows/database_matrix.yml",
    "content": "name: Database Tests\n\non: [push, pull_request]\n\njobs:\n  database-matrix:\n    runs-on: ubuntu-latest\n    \n    strategy:\n      fail-fast: false\n      matrix:\n        ruby: [\"3.4\", \"4.0\"]\n        rails: [\"7.2.0\", \"8.0.0\"]\n        database: [\"sqlite\", \"postgresql\", \"mysql\"]\n    \n    services:\n      postgres:\n        image: postgres:17\n        env:\n          POSTGRES_DB: panko_test\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: password\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n        ports:\n          - 5432:5432\n          \n      mysql:\n        image: mysql:8.0\n        env:\n          MYSQL_DATABASE: panko_test\n          MYSQL_ROOT_PASSWORD: password\n        options: >-\n          --health-cmd=\"mysqladmin ping\"\n          --health-interval=10s\n          --health-timeout=5s\n          --health-retries=3\n        ports:\n          - 3306:3306\n\n    steps:\n      - name: Install system dependencies\n        run: |\n          sudo apt update -y\n          sudo apt install -y libsqlite3-dev libpq-dev libmysqlclient-dev\n\n      - uses: actions/checkout@v6\n      \n      - name: Configure MySQL authentication\n        if: matrix.database == 'mysql'\n        run: |\n          # Configure MySQL to use mysql_native_password for Trilogy compatibility\n          mysql -h 127.0.0.1 -P 3306 -u root -ppassword -e \"\n            ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';\n            ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'password';\n            FLUSH PRIVILEGES;\n          \"\n        \n      - name: Set up Ruby ${{ matrix.ruby }}\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: ${{ matrix.ruby }}\n          bundler-cache: true\n          working-directory: .\n        env:\n          BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile\n          DB: ${{ matrix.database }}\n\n      - name: Compile & test\n        env:\n          BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile\n          DB: ${{ matrix.database }}\n          POSTGRES_HOST: localhost\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: password\n          POSTGRES_PORT: 5432\n          MYSQL_HOST: localhost\n          MYSQL_USER: root\n          MYSQL_PASSWORD: password\n          MYSQL_PORT: 3306\n        run: |\n          bundle exec rake\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: Docs Publishing\n\non:\n  push:\n    branches: [master]\n\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: \"pages\"\n  cancel-in-progress: false\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: docs\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Setup Ruby\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: \"4.0\"\n          bundler-cache: true\n          working-directory: docs\n\n      - name: Setup Pages\n        id: pages\n        uses: actions/configure-pages@v6\n\n      - name: Build with Jekyll\n        run: bundle exec jekyll build --baseurl \"${{ steps.pages.outputs.base_path }}\"\n        env:\n          JEKYLL_ENV: production\n\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v4\n        with:\n          path: docs/_site\n\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v5\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\n\non: [push, pull_request]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Install deps\n        run: |\n          sudo apt update -y\n          sudo apt install -y libsqlite3-dev\n\n      - name: Lint Ruby code\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: 3\n          bundler-cache: true\n      - run: |\n          bundle exec rake rubocop\n\n      - name: Lint C\n        uses: jidicula/clang-format-action@v4.16.0\n        with:\n          clang-format-version: \"16\"\n          check-path: \"ext/panko_serializer\"\n          fallback-style: \"Google\"\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Panko Serializer CI\n\non: [push, pull_request]\n\njobs:\n  tests:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        ruby: [\"3.2\", \"3.3\", \"3.4\", \"4.0\"]\n        rails: [\"7.2.0\", \"8.0.0\", \"8.1.0\"]\n\n    steps:\n      - name: Install deps\n        run: |\n          sudo apt update -y\n          sudo apt install -y libsqlite3-dev\n\n      - uses: actions/checkout@v6\n      - name: Set up Ruby ${{ matrix.ruby }}\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: ${{ matrix.ruby }}\n          bundler-cache: true\n          working-directory: .\n        env:\n          BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile\n\n      - name: Compile & test\n        env:\n          BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile\n        run: |\n          bundle exec rake\n"
  },
  {
    "path": ".gitignore",
    "content": "/.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*.bundle\n\n.DS_Store\n\n# rspec failure tracking\n.rspec_status\nnode_modules\n\n/docs/_site/\n/docs/.jekyll-cache/\n"
  },
  {
    "path": ".rspec",
    "content": "--format documentation\n--color\n"
  },
  {
    "path": ".rubocop.yml",
    "content": "# We want Exclude directives from different\n# config files to get merged, not overwritten\ninherit_mode:\n  merge:\n    - Exclude\n\nrequire:\n  - standard\n\nplugins:\n  - rubocop-performance\n  - standard-performance\n  - rubocop-rspec\n\ninherit_gem:\n  standard: config/base.yml\n  standard-performance: config/base.yml\n\nAllCops:\n  TargetRubyVersion: 3.1\n  SuggestExtensions: false\n  NewCops: disable\n  Exclude:\n    - ext/**/*\n    - gemfiles/**/*\n\n\nStyle/FrozenStringLiteralComment:\n  Enabled: true\n  EnforcedStyle: always\n  SafeAutoCorrect: true\n\n# TODO: need to work on specs.\nRSpec:\n  Enabled: false\n\nLint/ConstantDefinitionInBlock:\n  Exclude:\n    - spec/**/*\n"
  },
  {
    "path": "Appraisals",
    "content": "# frozen_string_literal: true\n\nappraise \"7.2.0\" do\n  gem \"activesupport\", \"~> 7.2.0\"\n  gem \"activemodel\", \"~> 7.2.0\"\n  gem \"activerecord\", \"~> 7.2.0\", group: :test\n\n  gem \"trilogy\"\n  gem \"sqlite3\", \"~> 1.4\"\nend\n\nappraise \"8.0.0\" do\n  gem \"activesupport\", \"~> 8.0.0\"\n  gem \"activemodel\", \"~> 8.0.0\"\n  gem \"activerecord\", \"~> 8.0.0\", group: :test\n\n  gem \"trilogy\"\n  gem \"sqlite3\", \">= 2.1\"\nend\n\nappraise \"8.1.0\" do\n  gem \"activesupport\", \"~> 8.1.0\"\n  gem \"activemodel\", \"~> 8.1.0\"\n  gem \"activerecord\", \"~> 8.1.0\", group: :test\n\n  gem \"trilogy\"\n  gem \"sqlite3\", \">= 2.1\"\nend\n"
  },
  {
    "path": "Gemfile",
    "content": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\ngemspec\n\ngroup :benchmarks do\n  gem \"vernier\"\n  gem \"stackprof\"\n  gem \"pg\"\n\n  gem \"benchmark-ips\"\n  gem \"memory_profiler\"\nend\n\ngroup :test do\n  gem \"faker\"\n  gem \"temping\"\nend\n\ngroup :development do\n  gem \"byebug\"\n  gem \"rake\"\n  gem \"rspec\", \"~> 3.0\"\n  gem \"rake-compiler\"\nend\n\ngroup :development, :test do\n  gem \"rubocop\"\n\n  gem \"standard\"\n  gem \"standard-performance\"\n  gem \"rubocop-performance\"\n  gem \"rubocop-rspec\"\nend\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017 Yosi Attias\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Panko\n\n![Build Status](https://github.com/yosiat/panko_serializer/workflows/Panko%20Serializer%20CI/badge.svg?branch=master)\n\nPanko is a library which is inspired by ActiveModelSerializers 0.9 for serializing ActiveRecord/Ruby objects to JSON strings, fast.\n\nTo achieve its [performance](https://panko.dev/performance):\n\n* Oj - Panko relies on Oj since it's fast and allows for incremental serialization using `Oj::StringWriter`\n* Serialization Descriptor - Panko computes most of the metadata ahead of time, to save time later in serialization.\n* Type casting — Panko does type casting by itself, instead of relying on ActiveRecord.\n\nTo dig deeper about the performance choices, read [Design Choices](https://panko.dev/design-choices).\n\n\nSupport\n-------\n\n- [Documentation](https://panko.dev/)\n- [Getting Started](https://panko.dev/getting-started)\n\nLicense\n-------\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n"
  },
  {
    "path": "Rakefile",
    "content": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rspec/core/rake_task\"\nrequire \"rubocop/rake_task\"\nrequire \"rake/extensiontask\"\n\ngem = Gem::Specification.load(File.dirname(__FILE__) + \"/panko_serializer.gemspec\")\n\nRake::ExtensionTask.new(\"panko_serializer\", gem) do |ext|\n  ext.lib_dir = \"lib/panko\"\nend\n\nGem::PackageTask.new(gem) do |pkg|\n  pkg.need_zip = pkg.need_tar = false\nend\n\nRSpec::Core::RakeTask.new(:spec)\nRake::Task[:spec].prerequisites << :compile\nRake::Task[:compile].prerequisites << :clean\n\ntask default: :spec\n\nRuboCop::RakeTask.new\n\nnamespace :benchmarks do\n  desc \"Run all benchmarks\"\n  task :all do\n    files = Dir[\"benchmarks/*.rb\", \"benchmarks/type_casts/*.rb\"].sort\n    files.each { |f| system(\"bundle\", \"exec\", \"ruby\", f) || abort(\"FAILED: #{f}\") }\n  end\n\n  desc \"Run benchmarks matching NAME (e.g., rake benchmarks:run[type_casts:postgresql])\"\n  task :run, [:name] do |_, args|\n    path = args[:name].tr(\":\", \"/\")\n    files = Dir[\"benchmarks/#{path}.rb\", \"benchmarks/#{path}/*.rb\"].sort\n    abort \"No benchmark files matching '#{args[:name]}'\" if files.empty?\n    files.each { |f| system(\"bundle\", \"exec\", \"ruby\", f) || abort(\"FAILED: #{f}\") }\n  end\nend\n"
  },
  {
    "path": "benchmarks/object_writer.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"support/benchmark\"\nrequire \"panko_serializer\"\n\nbenchmark(\"1 property, push_value\") do\n  writer = Panko::ObjectWriter.new\n  writer.push_object\n  writer.push_value \"value1\", \"key1\"\n  writer.pop\n  writer.output\nend\n\nbenchmark(\"2 properties, push_value\") do\n  writer = Panko::ObjectWriter.new\n  writer.push_object\n  writer.push_value \"value1\", \"key1\"\n  writer.push_value \"value2\", \"key2\"\n  writer.pop\n  writer.output\nend\n\nbenchmark(\"1 property, push_key+push_value\") do\n  writer = Panko::ObjectWriter.new\n  writer.push_object\n  writer.push_key \"key1\"\n  writer.push_value \"value1\"\n  writer.pop\n  writer.output\nend\n\nbenchmark(\"2 properties, push_key+push_value\") do\n  writer = Panko::ObjectWriter.new\n  writer.push_object\n  writer.push_key \"key1\"\n  writer.push_value \"value1\"\n  writer.push_key \"key2\"\n  writer.push_value \"value2\"\n  writer.pop\n  writer.output\nend\n\nbenchmark(\"Nested object\") do\n  writer = Panko::ObjectWriter.new\n  writer.push_object\n  writer.push_value \"value1\", \"key1\"\n  writer.push_object \"key2\"\n  writer.push_value \"value2\", \"key2\"\n  writer.pop\n  writer.pop\n  writer.output\nend\n"
  },
  {
    "path": "benchmarks/panko_json.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"support/datasets\"\n\nclass AuthorFastSerializer < Panko::Serializer\n  attributes :id, :name\nend\n\nclass PostFastSerializer < Panko::Serializer\n  attributes :id, :body, :title, :author_id, :created_at\nend\n\nclass PostFastWithMethodCallSerializer < Panko::Serializer\n  attributes :id, :body, :title, :author_id, :method_call\n\n  def method_call\n    object.id * 2\n  end\nend\n\nclass PostFastWithJsonSerializer < Panko::Serializer\n  attributes :id, :body, :title, :author_id, :created_at, :data\nend\n\nclass PostWithHasOneFastSerializer < Panko::Serializer\n  attributes :id, :body, :title, :author_id, :created_at\n\n  has_one :author, serializer: AuthorFastSerializer\nend\n\nclass AuthorWithHasManyFastSerializer < Panko::Serializer\n  attributes :id, :name\n\n  has_many :posts, serializer: PostFastSerializer\nend\n\nclass PostWithAliasFastSerializer < Panko::Serializer\n  attributes :new_id, :new_body, :new_title, :new_author_id, :new_created_at\nend\n\nbenchmark_with_records(\"Simple\", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostFastSerializer).to_json }\nbenchmark_with_records(\"HasOne\", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithHasOneFastSerializer).to_json }\nbenchmark_with_records(\"HasMany\", type: :authors) { |r| Panko::ArraySerializer.new(r, each_serializer: AuthorWithHasManyFastSerializer).to_json }\nbenchmark_with_records(\"MethodCall\", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostFastWithMethodCallSerializer).to_json }\nbenchmark_with_records(\"JSON column\", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostFastWithJsonSerializer).to_json }\nbenchmark_with_records(\"Except\", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithHasOneFastSerializer, except: [:title]).to_json }\nbenchmark_with_records(\"Only\", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithHasOneFastSerializer, only: [:id, :body, :author_id, :author]).to_json }\nbenchmark_with_records(\"Aliases\", type: :aliased_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithAliasFastSerializer).to_json }\n"
  },
  {
    "path": "benchmarks/panko_object.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"support/datasets\"\n\nclass AuthorFastSerializer < Panko::Serializer\n  attributes :id, :name\nend\n\nclass PostFastSerializer < Panko::Serializer\n  attributes :id, :body, :title, :author_id, :created_at\nend\n\nclass PostWithHasOneFastSerializer < Panko::Serializer\n  attributes :id, :body, :title, :author_id, :created_at\n\n  has_one :author, serializer: AuthorFastSerializer\nend\n\nclass PostWithAliasFastSerializer < Panko::Serializer\n  attributes :new_id, :new_body, :new_title, :new_author_id, :new_created_at\nend\n\nbenchmark_with_records(\"Simple\", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostFastSerializer).to_a }\nbenchmark_with_records(\"HasOne\", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithHasOneFastSerializer).to_a }\nbenchmark_with_records(\"Except\", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithHasOneFastSerializer, except: [:title]).to_a }\nbenchmark_with_records(\"Only\", type: :posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithHasOneFastSerializer, only: [:id, :body, :author_id, :author]).to_a }\nbenchmark_with_records(\"Aliases\", type: :aliased_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PostWithAliasFastSerializer).to_a }\n"
  },
  {
    "path": "benchmarks/plain_object.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"support/datasets\"\n\nclass PlainAuthorSerializer < Panko::Serializer\n  attributes :id, :name\nend\n\nclass PlainPostSerializer < Panko::Serializer\n  attributes :id, :body, :title, :author_id, :created_at\nend\n\nclass PlainPostWithMethodCallSerializer < Panko::Serializer\n  attributes :id, :body, :title, :author_id, :method_call\n\n  def method_call\n    object.id * 2\n  end\nend\n\nclass PlainPostWithHasOneSerializer < Panko::Serializer\n  attributes :id, :body, :title, :author_id, :created_at\n\n  has_one :author, serializer: PlainAuthorSerializer\nend\n\nbenchmark_with_records(\"Simple\", type: :plain_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PlainPostSerializer).to_json }\nbenchmark_with_records(\"HasOne\", type: :plain_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PlainPostWithHasOneSerializer).to_json }\nbenchmark_with_records(\"MethodCall\", type: :plain_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PlainPostWithMethodCallSerializer).to_json }\nbenchmark_with_records(\"Except\", type: :plain_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PlainPostWithHasOneSerializer, except: [:title]).to_json }\nbenchmark_with_records(\"Only\", type: :plain_posts) { |r| Panko::ArraySerializer.new(r, each_serializer: PlainPostWithHasOneSerializer, only: [:id, :body, :author_id, :author]).to_json }\n"
  },
  {
    "path": "benchmarks/support/benchmark.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nrequire \"benchmark/ips\"\nrequire \"memory_profiler\"\nrequire \"active_support/all\"\n\n# Enable YJIT if available (Ruby 3.1+)\nRubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)\n\n# ---------------------------------------------------------------------------\n# Constants\n# ---------------------------------------------------------------------------\n\n# Sizes to benchmark. Override with SIZE=n env var (single run).\n# @return [Array<Integer>]\nBENCHMARK_SIZES = ENV[\"SIZE\"] ? [Integer(ENV[\"SIZE\"])] : [50, 2300]\n\n# Registry of pre-loaded dataset slices, keyed by type symbol.\n# Populated by support/datasets.rb before any benchmark file runs.\n# @return [Hash{Symbol => Hash}]\nDATASETS = {}\n\n# Benchmark.ips measurement time in seconds (default 10).\n# @return [Integer]\nIPS_TIME = Integer(ENV.fetch(\"IPS_TIME\", 10))\n\n# Benchmark.ips warmup time in seconds (default 3).\n# @return [Integer]\nIPS_WARMUP = Integer(ENV.fetch(\"IPS_WARMUP\", 3))\n\n# ---------------------------------------------------------------------------\n# NoopWriter\n# ---------------------------------------------------------------------------\n\n# A no-op Oj::StringWriter stand-in used by benchmarks that test\n# serialization logic without paying JSON-string allocation costs.\nclass NoopWriter\n  # The last value passed to push_value.\n  # @return [Object, nil]\n  attr_reader :value\n\n  # Records +value+ without writing JSON.\n  #\n  # @param value [Object] the value to (not) write\n  # @param key [String, nil] ignored\n  # @return [void]\n  def push_value(value, key = nil)\n    @value = value\n  end\n\n  # No-op JSON push.\n  #\n  # @param value [String] ignored\n  # @param key [String, nil] ignored\n  # @return [nil]\n  def push_json(value, key = nil) # rubocop:disable Lint/UnusedMethodArgument\n    nil\n  end\nend\n\n# ---------------------------------------------------------------------------\n# Internal state\n# ---------------------------------------------------------------------------\n\n# @!visibility private\n@header_printed = false\n\n# @!visibility private\n@profile_blocks = []\n\n# ---------------------------------------------------------------------------\n# print_header\n# ---------------------------------------------------------------------------\n\n# Prints the benchmark table header once, deriving the section title from\n# the calling file's basename (without extension).\n#\n# @return [void]\ndef print_header\n  return if @header_printed\n\n  @header_printed = true\n\n  title = File.basename($PROGRAM_NAME, \".*\")\n  width = 78\n  puts \"=\" * width\n  puts \"  #{title}\".center(width)\n  puts \"=\" * width\n  puts \"benchmark                                                   ips     allocs   retained\"\n  puts \"-\" * width\nend\n\n# ---------------------------------------------------------------------------\n# benchmark\n# ---------------------------------------------------------------------------\n\n# Runs a single benchmark case, printing one formatted result row.\n#\n# Respects the BENCH env var: if set, only runs benchmarks whose +label+\n# contains the value as a case-insensitive substring.\n#\n# When PROFILE=cpu  : collects the block for a single StackProf run at exit.\n# When PROFILE=memory: runs MemoryProfiler and calls pretty_print immediately.\n# Normal mode       : disables GC, measures allocations + ips, prints a row.\n#\n# @param label [String] human-readable name shown in the output table\n# @yield the code under measurement (called many times by Benchmark.ips)\n# @return [void]\ndef benchmark(label, &block)\n  filter = ENV[\"BENCH\"]\n  return if filter && !label.downcase.include?(filter.downcase)\n\n  print_header\n\n  case ENV[\"PROFILE\"]\n  when \"cpu\"\n    @profile_blocks << [label, block]\n    return\n  when \"memory\"\n    report = MemoryProfiler.report(&block)\n    report.pretty_print\n    return\n  end\n\n  GC.start\n  GC.disable\n\n  memory_report = MemoryProfiler.report(&block)\n\n  ips_result = Benchmark.ips(IPS_TIME, IPS_WARMUP, true) do |x|\n    x.report(label, &block)\n  end\n\n  GC.enable\n\n  ips = ips_result.entries.first.ips.round(2)\n  allocs = memory_report.total_allocated\n  retained = memory_report.total_retained\n\n  ips_str = format(\"%.2f\", ips).reverse.gsub(/(\\d{3})(?=\\d)/, '\\1,').reverse\n  allocs_str = allocs.to_s.reverse.gsub(/(\\d{3})(?=\\d)/, '\\1,').reverse\n  retained_str = retained.to_s.reverse.gsub(/(\\d{3})(?=\\d)/, '\\1,').reverse\n\n  puts format(\"%-54s %12s %10s %10s\", label, ips_str, allocs_str, retained_str)\nend\n\n# ---------------------------------------------------------------------------\n# benchmark_with_records\n# ---------------------------------------------------------------------------\n\n# Iterates BENCHMARK_SIZES and runs one benchmark per size, automatically\n# slicing the dataset registered under +type+.\n#\n# @param label [String] benchmark label prefix (size + noun are appended)\n# @param type [Symbol] key into DATASETS (e.g. :posts)\n# @yield [records] the subset of records for this size\n# @yieldparam records [Array] first +n+ records from the dataset\n# @return [void]\ndef benchmark_with_records(label, type:, &block)\n  dataset = DATASETS.fetch(type)\n  data = dataset[:data]\n  noun = dataset[:noun]\n\n  BENCHMARK_SIZES.each do |n|\n    subset = data.first(n)\n    benchmark(\"#{label}, #{n} #{noun}\") { block.call(subset) }\n  end\nend\n\n# ---------------------------------------------------------------------------\n# run_cpu_profile\n# ---------------------------------------------------------------------------\n\n# Runs all blocks collected in PROFILE=cpu mode under a single StackProf\n# session and prints the results.\n#\n# @return [void]\ndef run_cpu_profile\n  return if @profile_blocks.empty?\n\n  require \"stackprof\"\n\n  combined = @profile_blocks.map { |_lbl, blk| blk }\n\n  profile = StackProf.run(mode: :cpu, raw: true) do\n    combined.each(&:call)\n  end\n\n  StackProf::Report.new(profile).print_text\nend\n\n# ---------------------------------------------------------------------------\n# at_exit\n# ---------------------------------------------------------------------------\n\nat_exit do\n  run_cpu_profile if ENV[\"PROFILE\"] == \"cpu\"\n  puts \"=\" * 78 if @header_printed\nend\n"
  },
  {
    "path": "benchmarks/support/datasets.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"benchmark\"\nrequire_relative \"setup\"\nrequire \"panko_serializer\"\n\n# --- AR datasets ---\n\nDATASETS[:posts] = {data: Post.all.includes(:author).to_a, noun: \"posts\"}\nDATASETS[:authors] = {data: Author.all.includes(:posts).to_a, noun: \"authors\"}\nDATASETS[:aliased_posts] = {data: PostWithAliasModel.all.to_a, noun: \"aliased posts\"}\n\n# --- Plain Ruby datasets (no AR dependency) ---\n\nclass PlainAuthor\n  attr_accessor :id, :name\nend\n\nclass PlainPost\n  attr_accessor :id, :body, :title, :created_at, :author_id\n  attr_reader :author\n\n  def author=(author)\n    @author = author\n    @author_id = author.id\n  end\nend\n\nplain_posts = 2300.times.map do |i|\n  author = PlainAuthor.new\n  author.id = i\n  author.name = \"Author #{i}\"\n\n  post = PlainPost.new\n  post.id = i\n  post.body = \"something about how password restrictions are evil\"\n  post.title = \"Your bank does not know how to do security\"\n  post.created_at = Time.now\n  post.author = author\n  post\nend\n\nDATASETS[:plain_posts] = {data: plain_posts, noun: \"plain posts\"}\n"
  },
  {
    "path": "benchmarks/support/setup.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"active_record\"\nrequire \"sqlite3\"\nrequire \"securerandom\"\n\n# Change the following to reflect your database settings\nActiveRecord::Base.establish_connection(\n  adapter: \"sqlite3\",\n  database: \":memory:\"\n)\n\n# Don't show migration output when constructing fake db\nActiveRecord::Migration.verbose = false\n\nActiveRecord::Schema.define do\n  create_table :authors, force: true do |t|\n    t.string :name\n    t.timestamps(null: false)\n  end\n\n  create_table :posts, force: true do |t|\n    t.text :body\n    t.string :title\n    t.references :author\n    t.json :data\n    t.timestamps(null: false)\n  end\nend\n\nclass Author < ActiveRecord::Base\n  has_many :posts\nend\n\nclass Post < ActiveRecord::Base\n  belongs_to :author\nend\n\nclass PostWithAliasModel < ActiveRecord::Base\n  self.table_name = \"posts\"\n\n  alias_attribute :new_id, :id\n  alias_attribute :new_body, :body\n  alias_attribute :new_title, :title\n  alias_attribute :new_author_id, :author_id\n  alias_attribute :new_created_at, :created_at\nend\n\nPost.transaction do\n  2300.times do\n    Post.create(\n      body: SecureRandom.hex(30),\n      title: SecureRandom.hex(20),\n      author: Author.create(name: SecureRandom.alphanumeric),\n      data: {a: 1, b: 2, c: 3}\n    )\n  end\nend\n"
  },
  {
    "path": "benchmarks/type_casts/generic.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"../support/benchmark\"\nrequire \"active_record\"\nrequire \"panko_serializer\"\n\nTime.zone = \"UTC\"\n\ndef bench_type(type_klass, from, to, label: type_klass.name)\n  converter = type_klass.new\n\n  benchmark(\"#{label} TypeCast\") do\n    Panko._type_cast(converter, from)\n  end\n\n  benchmark(\"#{label} NoTypeCast\") do\n    Panko._type_cast(converter, to)\n  end\nend\n\nbench_type ActiveRecord::Type::String, 1, \"1\"\nbench_type ActiveRecord::Type::Text, 1, \"1\"\nbench_type ActiveRecord::Type::ImmutableString, 1, \"1\"\nbench_type ActiveRecord::Type::Integer, \"1\", 1\nbench_type ActiveRecord::Type::BigInteger, \"1\", 1\nbench_type ActiveRecord::Type::Float, \"1.23\", 1.23\nbench_type ActiveRecord::Type::Decimal, \"123.45\", BigDecimal(\"123.45\")\nbench_type ActiveRecord::Type::Boolean, \"true\", true\nbench_type ActiveRecord::Type::Boolean, \"t\", true, label: \"ActiveRecord::Type::Boolean(t)\"\nbench_type ActiveRecord::Type::Date, \"2017-03-04\", Date.new(2017, 3, 4)\nbench_type ActiveRecord::Type::Time, \"2000-01-01 12:45:23\", Time.utc(2000, 1, 1, 12, 45, 23)\nbench_type ActiveRecord::Type::DateTime, \"2017-03-04 12:45:23\", Time.utc(2017, 3, 4, 12, 45, 23)\nbench_type ActiveRecord::Type::Binary, \"data\", \"data\".b\n\nif defined?(ActiveRecord::Type::Json)\n  bench_type ActiveRecord::Type::Json, '{\"a\":1}', {\"a\" => 1}\nend\n"
  },
  {
    "path": "benchmarks/type_casts/mysql.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"../support/benchmark\"\nrequire \"active_record\"\nrequire \"panko_serializer\"\n\nbegin\n  require \"active_record/connection_adapters/mysql2_adapter\"\nrescue LoadError\n  begin\n    require \"active_record/connection_adapters/trilogy_adapter\"\n  rescue LoadError\n    puts \"Skipping MySQL type_casts: mysql2/trilogy gem not installed\"\n    exit 0\n  end\nend\n\ndef bench_type(type_klass, from, to, label: type_klass.name)\n  converter = type_klass.new\n\n  benchmark(\"#{label} TypeCast\") do\n    Panko._type_cast(converter, from)\n  end\n\n  benchmark(\"#{label} NoTypeCast\") do\n    Panko._type_cast(converter, to)\n  end\nend\n\ndef bench_type_with_instance(instance, from, to, label:)\n  benchmark(\"#{label} TypeCast\") do\n    Panko._type_cast(instance, from)\n  end\n\n  benchmark(\"#{label} NoTypeCast\") do\n    Panko._type_cast(instance, to)\n  end\nend\n\n# --- MySQL-specific: UnsignedInteger ---\n\nif defined?(ActiveRecord::Type::UnsignedInteger)\n  bench_type ActiveRecord::Type::UnsignedInteger, \"42\", 42\nend\n\n# --- MySQL String with boolean coercion (true: \"1\", false: \"0\") ---\n\nmysql_string = ActiveModel::Type::String.new(true: \"1\", false: \"0\") # rubocop:disable Lint/BooleanSymbol\nbench_type_with_instance(mysql_string, 1, \"1\", label: \"MySQL::String (bool coercion)\")\n\nmysql_immutable = ActiveModel::Type::ImmutableString.new(true: \"1\", false: \"0\") # rubocop:disable Lint/BooleanSymbol\nbench_type_with_instance(mysql_immutable, 1, \"1\", label: \"MySQL::ImmutableString (bool coercion)\")\n\n# --- MySQL Text size variants ---\n\nbench_type_with_instance(ActiveRecord::Type::Text.new(limit: 2**8 - 1), 1, \"1\", label: \"MySQL::TinyText\")\nbench_type_with_instance(ActiveRecord::Type::Text.new(limit: 2**16 - 1), 1, \"1\", label: \"MySQL::Text\")\nbench_type_with_instance(ActiveRecord::Type::Text.new(limit: 2**24 - 1), 1, \"1\", label: \"MySQL::MediumText\")\nbench_type_with_instance(ActiveRecord::Type::Text.new(limit: 2**32 - 1), 1, \"1\", label: \"MySQL::LongText\")\n\n# --- MySQL Binary size variants ---\n\nbench_type_with_instance(ActiveModel::Type::Binary.new(limit: 2**8 - 1), \"data\", \"data\".b, label: \"MySQL::TinyBlob\")\nbench_type_with_instance(ActiveModel::Type::Binary.new(limit: 2**16 - 1), \"data\", \"data\".b, label: \"MySQL::Blob\")\nbench_type_with_instance(ActiveModel::Type::Binary.new(limit: 2**24 - 1), \"data\", \"data\".b, label: \"MySQL::MediumBlob\")\nbench_type_with_instance(ActiveModel::Type::Binary.new(limit: 2**32 - 1), \"data\", \"data\".b, label: \"MySQL::LongBlob\")\n\n# --- MySQL Float variants ---\n\nbench_type_with_instance(ActiveModel::Type::Float.new(limit: 24), \"1.23\", 1.23, label: \"MySQL::Float (single)\")\nbench_type_with_instance(ActiveModel::Type::Float.new(limit: 53), \"1.23\", 1.23, label: \"MySQL::Double (double)\")\n\n# --- MySQL Boolean (via tinyint(1) emulation) ---\n\nbench_type_with_instance(ActiveModel::Type::Boolean.new, 1, true, label: \"MySQL::Boolean (tinyint)\")\n"
  },
  {
    "path": "benchmarks/type_casts/postgresql.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"../support/benchmark\"\nrequire \"active_record\"\nrequire \"panko_serializer\"\n\nbegin\n  require \"pg\"\n  require \"active_record/connection_adapters/postgresql_adapter\"\nrescue LoadError\n  puts \"Skipping PostgreSQL type_casts: pg gem not installed\"\n  exit 0\nend\n\nTime.zone = \"UTC\"\n\ndef bench_type(type_klass, from, to, label: type_klass.name)\n  converter = type_klass.new\n\n  benchmark(\"#{label} TypeCast\") do\n    Panko._type_cast(converter, from)\n  end\n\n  benchmark(\"#{label} NoTypeCast\") do\n    Panko._type_cast(converter, to)\n  end\nend\n\nPG_OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID\n\nbench_type PG_OID::Uuid, \"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11\", \"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11\"\n\nif defined?(PG_OID::Jsonb)\n  bench_type PG_OID::Jsonb, '{\"a\":1}', {\"a\" => 1}\nend\n\nif defined?(PG_OID::Hstore)\n  bench_type PG_OID::Hstore, '\"a\"=>\"1\"', {\"a\" => \"1\"}\nend\n\nif defined?(PG_OID::Inet)\n  bench_type PG_OID::Inet, \"192.168.1.1\", IPAddr.new(\"192.168.1.1\")\nend\n\nif defined?(PG_OID::Cidr)\n  bench_type PG_OID::Cidr, \"192.168.1.0/24\", IPAddr.new(\"192.168.1.0/24\")\nend\n\nif defined?(PG_OID::Macaddr)\n  bench_type PG_OID::Macaddr, \"00:11:22:33:44:55\", \"00:11:22:33:44:55\"\nend\n\nif defined?(PG_OID::Point)\n  bench_type PG_OID::Point, \"(1.0,2.0)\", [1.0, 2.0]\nend\n\nif defined?(PG_OID::Money)\n  bench_type PG_OID::Money, \"$1,234.56\", BigDecimal(\"1234.56\")\nend\n\nif defined?(PG_OID::Bit)\n  bench_type PG_OID::Bit, \"101\", \"101\"\nend\n\nif defined?(PG_OID::BitVarying)\n  bench_type PG_OID::BitVarying, \"101\", \"101\"\nend\n\nif defined?(PG_OID::Xml)\n  bench_type PG_OID::Xml, \"<a/>\", \"<a/>\"\nend\n\nif defined?(PG_OID::Enum)\n  bench_type PG_OID::Enum, \"active\", \"active\"\nend\n\nif defined?(PG_OID::DateTime)\n  tz_type = PG_OID::DateTime.new\n  tz_converter = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(tz_type)\n\n  benchmark(\"PG DateTime+TZ TypeCast\") do\n    Panko._type_cast(tz_converter, \"2017-07-10 09:26:40.937392\")\n  end\nend\n"
  },
  {
    "path": "benchmarks/type_casts/sqlite.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"../support/benchmark\"\nrequire \"active_record\"\nrequire \"sqlite3\"\nrequire \"panko_serializer\"\n\nActiveRecord::Base.establish_connection(adapter: \"sqlite3\", database: \":memory:\")\n\nsqlite_int_type = ActiveRecord::Type::Integer.new(limit: 8)\n\nbenchmark(\"SQLite3 Integer(limit:8) TypeCast\") do\n  Panko._type_cast(sqlite_int_type, \"42\")\nend\n\nbenchmark(\"SQLite3 Integer(limit:8) NoTypeCast\") do\n  Panko._type_cast(sqlite_int_type, 42)\nend\n"
  },
  {
    "path": "docs/CNAME",
    "content": "panko.dev\n"
  },
  {
    "path": "docs/Gemfile",
    "content": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\ngem \"just-the-docs\"\n"
  },
  {
    "path": "docs/_config.yml",
    "content": "title: Panko Serializers\ndescription: High Performance JSON Serialization for ActiveRecord & Ruby Objects\nurl: https://panko.dev\nbaseurl: \"\"\n\ntheme: just-the-docs\n\nenable_copy_code_button: true\n\nnav_external_links:\n  - title: GitHub\n    url: https://github.com/yosiat/panko_serializer\n\nfooter_content: >-\n  <iframe src=\"https://ghbtns.com/github-btn.html?user=yosiat&amp;repo=panko_serializer&amp;type=star&amp;count=true&amp;size=medium\"\n  frameborder=\"0\" scrolling=\"0\" width=\"150\" height=\"20\" title=\"GitHub Stars\"></iframe>\n  <br>\n  Copyright &copy; 2026 Panko Serializer.\n  Distributed under the\n  <a href=\"https://github.com/yosiat/panko_serializer/blob/master/LICENSE.txt\">MIT License</a>.\n\nexclude:\n  - Gemfile\n  - Gemfile.lock\n"
  },
  {
    "path": "docs/associations.md",
    "content": "---\ntitle: Associations\nlayout: default\nnav_order: 6\nparent: Reference\n---\n\n# Associations\n\nA serializer can define it's own associations - both `has_many` and `has_one` to serialize under the context of the object.\n\nFor example:\n\n```ruby\nclass PostSerializer < Panko::Serializer\n  attributes :title, :body\n\n  has_one :author, serializer: AuthorSerializer\n  has_many :comments, each_serializer: CommentSerializer\nend\n```\n\n### Associations with aliases\n\nAn association key name can be aliased with the `name` option.\n\nFor example:\nthe `actual_author` property will be converted to `alias_author`.\n\n```ruby\nclass PostSerializer < Panko::Serializer\n  attributes :title, :body\n\n  has_one :actual_author, serializer: AuthorSerializer, name: :alias_author\n  has_many :comments, each_serializer: CommentSerializer\nend\n```\n\n### Inference\n\nPanko can find the type of the serializer by looking at the relationship name, so instead of specifying\nthe serializer at the above example, we can:\n\n```ruby\nclass PostSerializer < Panko::Serializer\n  attributes :title, :body\n\n  has_one :author\n  has_many :comments\nend\n```\n\nThe logic of inferencing is:\n\n-   Take the name of the relationship (for example - `:author` / `:comments`) singularize and camelize it.\n-   Look for const defined with the name above and \"Serializer\" suffix (by using `Object.const_get`).\n\n> 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`.\n\n## Nested Filters\n\nAs talked before, Panko allows you to filter the attributes of a serializer.\nBut Panko lets you take that step further, and filters the attributes of you associations so you can re-use your serializers in your application.\n\nFor 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.\n\nWe can declare tailored serializer for this, or we can re-use the above defined serializer - `PostSerializer` and use nested filters.\n\n```ruby\nposts = Post.all\n\nPanko::ArraySerializer.new(posts, each_serializer: PostSerializer, only: {\n  instance: [:title, :body, :author, :comments],\n  author: [:id],\n  comments: [:id],\n})\n```\n\nLet's dissect the `only` option we passed:\n\n-   `instance` - list of attributes (and associations) we want to serialize for the current instance of the serializer, in this case - `PostSerializer`.\n-   `author`, `comments` - here we specify the list of attributes we want to serialize for each association.\n\nIt's important to note that Nested Filters are recursive, in other words, we can filter the association's associations.\n\nFor example, `CommentSerializer` has an `has_one` association `Author`, and for each `comments.author` we can only serialize it's name.\n\n```ruby\nposts = Post.all\n\nPanko::ArraySerializer.new(posts, only: {\n  instance: [:title, :body, :author, :comments],\n  author: [:id],\n  comments: {\n    instance: [:id, :author],\n    author: [:name]\n  }\n})\n```\n\nAs you see now in `comments` the `instance` have different meaning, the `CommentSerializer`.\n"
  },
  {
    "path": "docs/attributes.md",
    "content": "---\ntitle: Attributes\nlayout: default\nnav_order: 5\nparent: Reference\n---\n\n# Attributes\n\nAttributes allow you to specify which record attributes you want to serialize.\n\nThere are two types of attributes:\n\n-   Field - simple columns defined on the record it self.\n-   Virtual/Method - this allows to include properties beyond simple fields.\n\n```ruby\nclass UserSerializer < Panko::Serializer\n  attributes :full_name\n\n  def full_name\n    \"#{object.first_name} #{object.last_name}\"\n   end\nend\n```\n\n## Field Attributes\n\nUsing field attributes you can control which columns of the given ActiveRecord object you want to serialize.\n\nInstead 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)).\n\n## Method Attributes\n\nMethod attributes are used when your serialized values can be derived from the object you are serializing.\n\nThe serializer's attribute methods can access the object being serialized as `object`:\n\n```ruby\nclass PostSerializer < Panko::Serializer\n  attributes :author_name\n\n  def author_name\n    \"#{object.author.first_name} #{object.author.last_name}\"\n  end\nend\n```\n\nAnother useful thing you can pass your serializer is `context`, a `context` is a bag of data whom your serializer may need.\n\nFor example, here we will pass feature flags:\n\n```ruby\nclass UserSerializer < Panko::Serializer\n  attributes :id, :email\n\n  def feature_flags\n    context[:feature_flags]\n  end\nend\n\nserializer = UserSerializer.new(context: {\n  feature_flags: FeatureFlags.all\n})\n\nserializer.serialize(User.first)\n```\n\n## Filters\n\nFilters allows us to reduce the amount of attributes we can serialize, therefore reduce the data usage & performance of serializing.\n\nThere are two types of filters:\n\n-   only - use those attributes **only** and nothing else.\n-   except - all attributes **except** those attributes.\n\nUsage example:\n\n```ruby\nclass UserSerializer < Panko::Serializer\n  attributes :id, :name, :email\nend\n\n# this line will return { 'name': '..' }\nUserSerializer.new(only: [:name]).serialize(User.first)\n\n# this line will return { 'id': '..', 'email': ... }\nUserSerializer.new(except: [:name]).serialize(User.first)\n```\n\n> **Note** that if you want to user filter on an associations, the `:name` property is not taken into account.\nIf you have a `has_many :state_transitions, name: :history` association defined, the key to use in filters is\n`:state_transitions` (e.g. `{ except: [:state_transitions] }`).\n\n## Filters For\n\nSometimes you find yourself having the same filtering logic in actions. In order to\nsolve this duplication, Panko allows you to write the filters in the serializer.\n\n```ruby\nclass UserSerializer < Panko::Serializer\n  attributes :id, :name, :email\n\n  def self.filters_for(context, scope)\n    {\n      only: [:name]\n    }\n  end\nend\n\n# this line will return { 'name': '..' }\nUserSerializer.serialize(User.first)\n```\n\n> See discussion in: [https://github.com/yosiat/panko_serializer/issues/16](https://github.com/yosiat/panko_serializer/issues/16)\n\n## Aliases\n\nLet'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:\n\n```ruby\nclass PostSerializer < Panko::Serializer\n  attributes :published_at\n\n  def published_at\n    object.created_at\n  end\nend\n```\n\nThe downside of this approach is that `created_at` skips Panko's type casting, therefore we get a direct hit on performance.\n\nTo fix this, we can use aliases:\n\n```ruby\nclass PostSerializer < Panko::Serializer\n  aliases created_at: :published_at\nend\n```\n"
  },
  {
    "path": "docs/design-choices.md",
    "content": "---\ntitle: Design Choices\nlayout: default\nnav_order: 4\n---\n\n# Design Choices\n\nIn 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).\n\nIts performance is achieved by:\n\n-   `Oj::StringWriter` - I will elaborate later.\n-   Type casting — instead of relying on ActiveRecord to do its type cast, Panko is doing it by itself.\n-   Figuring out the metadata, ahead of time — therefore, we ask less questions during the `serialization loop`.\n\n## Serialization overview\n\nFirst, let's start with an overview. Let's say we want to serialize an `User` object, which has\n`first_name`, `last_name`, `age`, and `email` properties.\n\nThe serializer definition will be something like this:\n\n```ruby\nclass UserSerializer < Panko::Serializer\n  attributes :name, :age, :email\n\n  def name\n    \"#{object.first_name} #{object.last_name}\"\n  end\nend\n```\n\nAnd the usage of this serializer will be:\n\n```ruby\n# fetch user from database\nuser = User.first\n\n# create serializer, with empty options\nserializer = UserSerializer.new\n\n# serialize to JSON\nserializer.serialize_to_json(user)\n```\n\nLet's go over the steps that Panko will execute behind the scenes for this flow.\n_I will skip the serializer definition part, because it's fairly simple and straightforward (see `lib/panko/serializer.rb`)._\n\nFirst step, while initializing the UserSerializer, we will create a **Serialization Descriptor** for this class.\nSerialization Descriptor's goal is to answer those questions:\n\n-   Which fields do we have? In our case, `:age`, `:email`.\n-   Which method fields do we have? In our case `:name`.\n-   Which associations do we have (and their serialization descriptors)?\n\nThe serialization description is also responsible for filtering the attributes (`only` \\\\ `except`).\n\nNow, 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.\n\nIn C land, we take the `user` object and the serialization descriptor, and start the serialization process which is separated to 4 parts:\n\n-   Serializing Fields - looping through serialization descriptor's `fields` and read them from the ActiveRecord object (see `Type Casting`) and write them to the writer.\n-   Serializing Method Fields - creating (a cached) serializer instance, setting its `@object` and `@context`, calling all the method fields and writing them to the writer.\n-   Serializing associations — this is simple, once we have fields + method fields, we just repeat the process.\n\nOnce this is finished, we have a nice JSON string.\nNow let's dig deeper.\n\n## Interesting parts\n\n### Oj::StringWriter\n\nIf you read the code of ActiveRecord serialization code in Ruby, you will observe this flow:\n\n1.  Get an array of ActiveRecord objects (`User.all` for example).\n2.  Build a new array of hashes where each hash is an `User` with the attributes we selected.\n3.  The JSON serializer, takes this array of hashes and loop them, and converts it to a JSON string.\n\nThis entire process is expensive in terms of Memory & CPU, and this where the combination of Panko and Oj::StringWriter really shines.\n\nIn Panko, the serialization process of the above is:\n\n1.  Get an array of ActiveRecord objects (`User.all` for example).\n2.  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.\n3.  Get from `Oj::StringWriter` the completed JSON string — which is a no-op, since `Oj::StringWriter` already built the string.\n\n### Figuring out the metadata, ahead of time.\n\nAnother observation I noticed in the Ruby serializers is that they ask and do a lot in a serialization loop:\n\n-   Is this field a method? is it a property?\n-   Which fields and associations do I need for the serializer to consider the `only` and `except` options?\n-   What is the serializer of this has_one association?\n\nPanko tries to ask the bare minimum in serialization by building `Serialization Descriptor` for each serialization and caching it.\n\nThe 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`).\n\n### Type Casting\n\nThis is the final part, which helped yield most of the performance improvements.\nIn ActiveRecord, when we read the value of an attribute, it does type casting of the DB value to its real Ruby type.\n\nFor example, time strings are converted to Time objects, Strings are duplicated, and Integers are converted from their values to Number.\n\nThis 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\".\n\nIf 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.\n\nWhat Panko does is that if we have ActiveRecord type string, we won't duplicate it.\nIf we have an integer string value, we will convert it to an integer, and the same goes for other types.\n\nAll of these conversions are done in C, which of course yields a big performance improvement.\n\n#### Time type casting\n\nWhile you read Panko source code, you will encounter the time type casting and immediately you will have a \"WTF?\" moment.\n\nThe 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.\n\nThe time type casting works as follows:\n\n-   If it's a string that ends with `Z`, and the strings matches the UTC ISO8601 regex, then we just return the string.\n-   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).\n-   If it's none of the above, I will let ActiveRecord type casting do it's magic.\n"
  },
  {
    "path": "docs/getting-started.md",
    "content": "---\ntitle: Getting Started\nlayout: default\nnav_order: 2\n---\n\n# Getting Started\n\n## Installation\n\nTo install Panko, all you need is to add it to your Gemfile:\n\n```ruby\ngem \"panko_serializer\"\n```\n\nThen, install it on the command line:\n\n```\nbundle install\n```\n\n## Creating your first serializer\n\nLet's create a serializer and use it inside of a Rails controller:\n\n```ruby\nclass PostSerializer < Panko::Serializer\n  attributes :title\nend\n\nclass UserSerializer < Panko::Serializer\n  attributes :id, :name, :age\n\n  has_many :posts, serializer: PostSerializer\nend\n```\n\n### Serializing an object\n\nAnd now serialize a single object:\n\n```ruby\n# Using Oj serializer\nPostSerializer.new.serialize_to_json(Post.first)\n\n# or, similar to #serializable_hash\nPostSerializer.new.serialize(Post.first).to_json\n```\n\n### Using the serializers in a controller\n\nAs you can see, defining serializers is simple and resembles ActiveModelSerializers 0.9.\nTo utilize the `UserSerializer` inside a Rails controller and serialize some users, all we need to do is:\n\n```ruby\nclass UsersController < ApplicationController\n def index\n   users = User.includes(:posts).all\n   render json: Panko::ArraySerializer.new(users, each_serializer: UserSerializer).to_json\n end\nend\n```\n\nAnd voila, we have an endpoint which serializes users using Panko!\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\ntitle: Introduction\nlayout: default\nnav_order: 1\n---\n\n# Introduction\n\nPanko is a library which is inspired by ActiveModelSerializers 0.9 for serializing ActiveRecord/Ruby objects to JSON strings, fast.\n\nTo achieve it's [performance]({% link performance.md %}):\n\n-   Oj - Panko relies on Oj since it's fast and allow to serialize incrementally using `Oj::StringWriter`.\n-   Serialization Descriptor - Panko computes most of the metadata ahead of time, to save time later in serialization.\n-   Type casting — Panko does type casting by itself, instead of relying on ActiveRecord.\n"
  },
  {
    "path": "docs/performance.md",
    "content": "---\ntitle: Performance\nlayout: default\nnav_order: 3\n---\n\n# Performance\n\nThe performance of Panko is measured using microbenchmarks and load testing.\n\n## Microbenchmarks\n\nThe following microbenchmarks are run on MacBook Pro (16-inch, 2021, M1 Max), Ruby 3.2.0 with Rails 7.0.5\ndemonstrating the performance of ActiveModelSerializers 0.10.13 and Panko 0.8.0.\n\n| Benchmark         | AMS ip/s | Panko ip/s |\n| ----------------- | -------- | ---------- |\n| Simple_Posts_2300 | 11.72    | 523.05     |\n| Simple_Posts_50   | 557.29   | 23,011.9   |\n| HasOne_Posts_2300 | 5.91     | 233.44     |\n| HasOne_Posts_50   | 285.8    | 10,362.79  |\n\n## Real-world benchmark\n\nThe real-world benchmark here is an endpoint which serializes 7,884 entries with 48 attributes and no associations.\nThe benchmark took place in an environment that simulates production environment and run using `wrk` from machine on the same cluster.\n\n| Metric             | AMS   | Panko |\n| ------------------ | ----- | ----- |\n| Avg Response Time  | 4.89s | 1.48s |\n| Max Response Time  | 5.42s | 1.83s |\n| 99th Response Time | 5.42s | 1.74s |\n| Total Requests     | 61    | 202   |\n\n_Thanks to [Bringg](https://www.bringg.com) for providing the infrastructure for the benchmarks._\n"
  },
  {
    "path": "docs/reference.md",
    "content": "---\ntitle: Reference\nlayout: default\nnav_order: 5\nhas_children: true\n---\n\n# Reference\n\nDetailed reference documentation for Panko Serializer's features.\n"
  },
  {
    "path": "docs/response-bag.md",
    "content": "---\ntitle: Response\nlayout: default\nnav_order: 7\nparent: Reference\n---\n\n# Response\n\nLet's say you have some JSON payload which is constructed using Panko serialization result,\nlike this:\n\n```ruby\nclass PostsController < ApplicationController\n  def index\n   posts = Post.all\n   render json: {\n     success: true,\n     total_count: posts.count,\n     posts: Panko::ArraySerializer.new(posts, each_serializer: PostSerializer).to_json\n   }\n  end\nend\n```\n\nThe output of the above will be a JSON string (for `posts`) inside a JSON string and this were `Panko::Response` shines.\n\n```ruby\nclass PostsController < ApplicationController\n  def index\n   posts = Post.all\n   render json: Panko::Response.new(\n     success: true,\n     total_count: posts.count,\n     posts: Panko::ArraySerializer.new(posts, each_serializer: PostSerializer)\n   )\n  end\nend\n```\n\nAnd everything will work as expected!\n\nFor a single object serialization, we need to use a different API (since `Panko::Serializer` doesn't accept an object in it's constructor):\n\n```ruby\nclass PostsController < ApplicationController\n  def show\n    post = Post.find(params[:id])\n\n    render(\n      json: Panko::Response.create do |r|\n        {\n          success: true,\n          post: r.serializer(post, PostSerializer)\n        }\n      end\n    )\n  end\nend\n```\n\n## JsonValue\n\nLet's take the above example further, we will serialize the posts and cache it as JSON string in our Cache.\nNow, you can wrap the cached value with `Panko::JsonValue`, like here:\n\n```ruby\nclass PostsController < ApplicationController\n  def index\n   posts = Cache.get(\"/posts\")\n\n   render json: Panko::Response.new(\n     success: true,\n     total_count: posts.count,\n     posts: Panko::JsonValue.from(posts)\n   )\n  end\nend\n```\n"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/active_record.c",
    "content": "#include \"active_record.h\"\n\nstatic ID attributes_id;\nstatic ID types_id;\nstatic ID additional_types_id;\nstatic ID values_id;\nstatic ID delegate_hash_id;\n\nstatic ID value_before_type_cast_id;\nstatic ID type_id;\n\nstatic ID fetch_id;\n\nstruct attributes {\n  // Hash\n  VALUE attributes_hash;\n  size_t attributes_hash_size;\n\n  // Hash\n  VALUE types;\n  // Hash\n  VALUE additional_types;\n  // heuristics\n  bool tryToReadFromAdditionalTypes;\n\n  // Rails <8: Hash\n  // Rails >=8: ActiveRecord::Result::IndexedRow\n  VALUE values;\n\n  // Hash\n  VALUE indexed_row_column_indexes;\n  // Array or NIL\n  VALUE indexed_row_row;\n  bool is_indexed_row;\n};\n\nstruct attributes init_context(VALUE obj) {\n  volatile VALUE attributes_set = rb_ivar_get(obj, attributes_id);\n  volatile VALUE attributes_hash = rb_ivar_get(attributes_set, attributes_id);\n\n  struct attributes attrs = (struct attributes){\n      .attributes_hash =\n          PANKO_EMPTY_HASH(attributes_hash) ? Qnil : attributes_hash,\n      .attributes_hash_size = 0,\n      .types = rb_ivar_get(attributes_set, types_id),\n      .additional_types = rb_ivar_get(attributes_set, additional_types_id),\n      .tryToReadFromAdditionalTypes =\n          PANKO_EMPTY_HASH(rb_ivar_get(attributes_set, additional_types_id)) ==\n          false,\n      .values = rb_ivar_get(attributes_set, values_id),\n      .is_indexed_row = false,\n      .indexed_row_column_indexes = Qnil,\n      .indexed_row_row = Qnil,\n  };\n\n  if (attrs.attributes_hash != Qnil) {\n    attrs.attributes_hash_size = RHASH_SIZE(attrs.attributes_hash);\n  }\n\n  if (strcmp(rb_class2name(CLASS_OF(attrs.values)),\n             \"ActiveRecord::Result::IndexedRow\") == 0) {\n    volatile VALUE indexed_row_column_indexes =\n        rb_ivar_get(attrs.values, rb_intern(\"@column_indexes\"));\n    volatile VALUE indexed_row_row =\n        rb_ivar_get(attrs.values, rb_intern(\"@row\"));\n\n    attrs.indexed_row_column_indexes = indexed_row_column_indexes;\n    attrs.indexed_row_row = indexed_row_row;\n    attrs.is_indexed_row = true;\n  }\n\n  return attrs;\n}\n\nVALUE _read_value_from_indexed_row(struct attributes attributes_ctx,\n                                   volatile VALUE member) {\n  volatile VALUE value = Qnil;\n\n  if (NIL_P(attributes_ctx.indexed_row_column_indexes) ||\n      NIL_P(attributes_ctx.indexed_row_row)) {\n    return value;\n  }\n\n  volatile VALUE column_index =\n      rb_hash_aref(attributes_ctx.indexed_row_column_indexes, member);\n\n  if (NIL_P(column_index)) {\n    return value;\n  }\n\n  volatile VALUE row = attributes_ctx.indexed_row_row;\n  if (NIL_P(row)) {\n    return value;\n  }\n\n  return RARRAY_AREF(row, NUM2INT(column_index));\n}\n\nVALUE read_attribute(struct attributes attributes_ctx, Attribute attribute,\n                     volatile VALUE* isJson) {\n  volatile VALUE member, value;\n\n  member = attribute->name_str;\n  value = Qnil;\n\n  if (\n      // we have attributes_hash\n      !NIL_P(attributes_ctx.attributes_hash)\n      // It's not empty\n      && (attributes_ctx.attributes_hash_size > 0)) {\n    volatile VALUE attribute_metadata =\n        rb_hash_aref(attributes_ctx.attributes_hash, member);\n\n    if (attribute_metadata != Qnil) {\n      value = rb_ivar_get(attribute_metadata, value_before_type_cast_id);\n\n      if (NIL_P(attribute->type)) {\n        attribute->type = rb_ivar_get(attribute_metadata, type_id);\n      }\n    }\n  }\n\n  if (NIL_P(value) && !NIL_P(attributes_ctx.values)) {\n    if (attributes_ctx.is_indexed_row == true) {\n      value = _read_value_from_indexed_row(attributes_ctx, member);\n    } else {\n      value = rb_hash_aref(attributes_ctx.values, member);\n    }\n  }\n\n  if (NIL_P(attribute->type) && !NIL_P(value)) {\n    if (attributes_ctx.tryToReadFromAdditionalTypes == true) {\n      attribute->type = rb_hash_aref(attributes_ctx.additional_types, member);\n    }\n\n    if (!NIL_P(attributes_ctx.types) && NIL_P(attribute->type)) {\n      attribute->type = rb_hash_aref(attributes_ctx.types, member);\n    }\n  }\n\n  if (!NIL_P(attribute->type) && !NIL_P(value)) {\n    return type_cast(attribute->type, value, isJson);\n  }\n\n  return value;\n}\n\nvoid active_record_attributes_writer(VALUE obj, VALUE attributes,\n                                     EachAttributeFunc write_value,\n                                     VALUE writer) {\n  long i;\n  struct attributes attributes_ctx = init_context(obj);\n  volatile VALUE record_class = CLASS_OF(obj);\n\n  for (i = 0; i < RARRAY_LEN(attributes); i++) {\n    volatile VALUE raw_attribute = RARRAY_AREF(attributes, i);\n    Attribute attribute = PANKO_ATTRIBUTE_READ(raw_attribute);\n    attribute_try_invalidate(attribute, record_class);\n\n    volatile VALUE isJson = Qfalse;\n    volatile VALUE value = read_attribute(attributes_ctx, attribute, &isJson);\n\n    write_value(writer, attr_name_for_serialization(attribute), value, isJson);\n  }\n}\n\nvoid init_active_record_attributes_writer(VALUE mPanko) {\n  attributes_id = rb_intern(\"@attributes\");\n  delegate_hash_id = rb_intern(\"@delegate_hash\");\n  values_id = rb_intern(\"@values\");\n  types_id = rb_intern(\"@types\");\n  additional_types_id = rb_intern(\"@additional_types\");\n  type_id = rb_intern(\"@type\");\n  value_before_type_cast_id = rb_intern(\"@value_before_type_cast\");\n  fetch_id = rb_intern(\"fetch\");\n}\n\nvoid panko_init_active_record(VALUE mPanko) {\n  init_active_record_attributes_writer(mPanko);\n  panko_init_type_cast(mPanko);\n}"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/active_record.h",
    "content": "#pragma once\n\n#include <ruby.h>\n#include <stdbool.h>\n\n#include \"../common.h\"\n#include \"common.h\"\n#include \"serialization_descriptor/attribute.h\"\n#include \"type_cast/type_cast.h\"\n\nextern void active_record_attributes_writer(VALUE object, VALUE attributes,\n                                            EachAttributeFunc func,\n                                            VALUE writer);\n\nvoid init_active_record_attributes_writer(VALUE mPanko);\n\nvoid panko_init_active_record(VALUE mPanko);"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/attributes_writer.c",
    "content": "#include \"attributes_writer.h\"\n\nstatic bool types_initialized = false;\nstatic VALUE ar_base_type = Qundef;\n\nVALUE init_types(VALUE v) {\n  if (types_initialized == true) {\n    return Qundef;\n  }\n\n  types_initialized = true;\n\n  volatile VALUE ar_type =\n      rb_const_get_at(rb_cObject, rb_intern(\"ActiveRecord\"));\n\n  ar_base_type = rb_const_get_at(ar_type, rb_intern(\"Base\"));\n  rb_global_variable(&ar_base_type);\n\n  return Qundef;\n}\n\nAttributesWriter create_attributes_writer(VALUE object) {\n  // If ActiveRecord::Base can't be found it will throw error\n  int isErrored;\n  rb_protect(init_types, Qnil, &isErrored);\n\n  if (ar_base_type != Qundef &&\n      rb_obj_is_kind_of(object, ar_base_type) == Qtrue) {\n    return (AttributesWriter){\n        .object_type = ActiveRecord,\n        .write_attributes = active_record_attributes_writer};\n  }\n\n  if (!RB_SPECIAL_CONST_P(object) && BUILTIN_TYPE(object) == T_HASH) {\n    return (AttributesWriter){.object_type = Hash,\n                              .write_attributes = hash_attributes_writer};\n  }\n\n  return (AttributesWriter){.object_type = Plain,\n                            .write_attributes = plain_attributes_writer};\n\n  return create_empty_attributes_writer();\n}\n\nvoid empty_write_attributes(VALUE obj, VALUE attributes, EachAttributeFunc func,\n                            VALUE writer) {}\n\nAttributesWriter create_empty_attributes_writer() {\n  return (AttributesWriter){.object_type = UnknownObjectType,\n                            .write_attributes = empty_write_attributes};\n}\n\nvoid init_attributes_writer(VALUE mPanko) {\n  init_active_record_attributes_writer(mPanko);\n}\n"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/attributes_writer.h",
    "content": "#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\nenum ObjectType {\n  UnknownObjectType = 0,\n  ActiveRecord = 1,\n  Plain = 2,\n  Hash = 3\n};\n\ntypedef struct _AttributesWriter {\n  enum ObjectType object_type;\n\n  void (*write_attributes)(VALUE object, VALUE attributes,\n                           EachAttributeFunc func, VALUE context);\n} AttributesWriter;\n\n/**\n * Infers the attributes writer from the object type\n */\nAttributesWriter create_attributes_writer(VALUE object);\n\n/**\n * Creates empty writer\n * Useful when the writer is not known, and you need init something\n */\nAttributesWriter create_empty_attributes_writer();\n\nvoid init_attributes_writer(VALUE mPanko);\n"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/common.c",
    "content": "#include \"common.h\"\n\nVALUE attr_name_for_serialization(Attribute attribute) {\n  volatile VALUE name_str = attribute->name_str;\n  if (attribute->alias_name != Qnil) {\n    name_str = attribute->alias_name;\n  }\n\n  return name_str;\n}\n"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/common.h",
    "content": "#pragma once\n\n#include \"../serialization_descriptor/attribute.h\"\n#include \"ruby.h\"\n\ntypedef void (*EachAttributeFunc)(VALUE writer, VALUE name, VALUE value,\n                                  VALUE isJson);\n\nVALUE attr_name_for_serialization(Attribute attribute);\n"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/hash.c",
    "content": "#include \"hash.h\"\n\nvoid hash_attributes_writer(VALUE obj, VALUE attributes,\n                            EachAttributeFunc write_value, VALUE writer) {\n  long i;\n  for (i = 0; i < RARRAY_LEN(attributes); i++) {\n    volatile VALUE raw_attribute = RARRAY_AREF(attributes, i);\n    Attribute attribute = attribute_read(raw_attribute);\n\n    write_value(writer, attr_name_for_serialization(attribute),\n                rb_hash_aref(obj, attribute->name_str), Qfalse);\n  }\n}\n"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/hash.h",
    "content": "#pragma once\n\n#include \"common.h\"\n#include \"ruby.h\"\n\nvoid hash_attributes_writer(VALUE obj, VALUE attributes, EachAttributeFunc func,\n                            VALUE writer);\n"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/plain.c",
    "content": "#include \"plain.h\"\n\nvoid plain_attributes_writer(VALUE obj, VALUE attributes,\n                             EachAttributeFunc write_value, VALUE writer) {\n  long i;\n  for (i = 0; i < RARRAY_LEN(attributes); i++) {\n    volatile VALUE raw_attribute = RARRAY_AREF(attributes, i);\n    Attribute attribute = attribute_read(raw_attribute);\n\n    write_value(writer, attr_name_for_serialization(attribute),\n                rb_funcall(obj, attribute->name_id, 0), Qfalse);\n  }\n}\n"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/plain.h",
    "content": "#pragma once\n\n#include \"common.h\"\n#include \"ruby.h\"\n\nvoid plain_attributes_writer(VALUE obj, VALUE attributes,\n                             EachAttributeFunc func, VALUE writer);\n"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/type_cast/time_conversion.c",
    "content": "#include \"time_conversion.h\"\n\nconst int YEAR_REGION = 1;\nconst int MONTH_REGION = 2;\nconst int DAY_REGION = 3;\nconst int HOUR_REGION = 4;\nconst int MINUTE_REGION = 5;\nconst int SECOND_REGION = 6;\n\nstatic regex_t* iso8601_time_regex;\nstatic regex_t* ar_iso_datetime_regex;\n\nVALUE is_iso8601_time_string(const char* value) {\n  const UChar *start, *range, *end;\n  OnigPosition r;\n\n  const UChar* str = (const UChar*)(value);\n\n  end = str + strlen(value);\n  start = str;\n  range = end;\n  r = onig_search(iso8601_time_regex, str, end, start, range, NULL,\n                  ONIG_OPTION_NONE);\n\n  return r >= 0 ? Qtrue : Qfalse;\n}\n\nvoid append_region_str(const char* source, char** to, int regionBegin,\n                       int regionEnd) {\n  long iter = 0;\n  for (iter = regionBegin; iter < regionEnd; iter++) {\n    *(*to)++ = source[iter];\n  }\n}\n\nbool is_iso_ar_iso_datetime_string_fast_case(const char* value) {\n  return (\n      // year\n      isdigit(value[0]) && isdigit(value[1]) && isdigit(value[2]) &&\n      isdigit(value[3]) && value[4] == '-' &&\n      // month\n      isdigit(value[5]) && isdigit(value[6]) && value[7] == '-' &&\n      // mday\n      isdigit(value[8]) && isdigit(value[9]) && value[10] == ' ' &&\n\n      // hour\n      isdigit(value[11]) && isdigit(value[12]) && value[13] == ':' &&\n      // minute\n      isdigit(value[14]) && isdigit(value[15]) && value[16] == ':' &&\n      // seconds\n      isdigit(value[17]) && isdigit(value[18]));\n}\n\nbool is_iso_ar_iso_datetime_string_slow_case(const char* value) {\n  const UChar *start, *range, *end;\n  OnigPosition r;\n  OnigRegion* region = onig_region_new();\n\n  const UChar* str = (const UChar*)(value);\n\n  end = str + strlen(value);\n  start = str;\n  range = end;\n  r = onig_search(ar_iso_datetime_regex, str, end, start, range, region,\n                  ONIG_OPTION_NONE);\n\n  onig_region_free(region, 1);\n\n  return (r >= 0);\n}\n\nVALUE iso_ar_iso_datetime_string(const char* value) {\n  if (is_iso_ar_iso_datetime_string_fast_case(value) == true ||\n      is_iso_ar_iso_datetime_string_slow_case(value) == true) {\n    volatile VALUE output;\n\n    char buf[24] = \"\";\n    char* cur = buf;\n\n    append_region_str(value, &cur, 0, 4);\n    *cur++ = '-';\n\n    append_region_str(value, &cur, 5, 7);\n    *cur++ = '-';\n\n    append_region_str(value, &cur, 8, 10);\n    *cur++ = 'T';\n\n    append_region_str(value, &cur, 11, 13);\n    *cur++ = ':';\n\n    append_region_str(value, &cur, 14, 16);\n    *cur++ = ':';\n\n    append_region_str(value, &cur, 17, 19);\n\n    *cur++ = '.';\n    if (value[19] == '.' && isdigit(value[20])) {\n      if (isdigit(value[20])) {\n        *cur++ = value[20];\n      } else {\n        *cur++ = '0';\n      }\n\n      if (isdigit(value[21])) {\n        *cur++ = value[21];\n      } else {\n        *cur++ = '0';\n      }\n\n      if (isdigit(value[22])) {\n        *cur++ = value[22];\n      } else {\n        *cur++ = '0';\n      }\n    } else {\n      *cur++ = '0';\n      *cur++ = '0';\n      *cur++ = '0';\n    }\n    *cur++ = 'Z';\n\n    output = rb_str_new(buf, cur - buf);\n    return output;\n  }\n\n  return Qnil;\n}\n\nvoid build_regex(OnigRegex* reg, const UChar* pattern) {\n  OnigErrorInfo einfo;\n\n  int r = onig_new(reg, pattern, pattern + strlen((char*)pattern),\n                   ONIG_OPTION_DEFAULT, ONIG_ENCODING_ASCII,\n                   ONIG_SYNTAX_DEFAULT, &einfo);\n\n  if (r != ONIG_NORMAL) {\n    char s[ONIG_MAX_ERROR_MESSAGE_LEN];\n    onig_error_code_to_str((UChar*)s, r, &einfo);\n    printf(\"ERROR: %s\\n\", s);\n  }\n}\n\nvoid panko_init_time(VALUE mPanko) {\n  const UChar *ISO8601_PATTERN, *AR_ISO_DATETIME_PATTERN;\n\n  ISO8601_PATTERN =\n      (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)?)?)?)?$\";\n\n  build_regex(&iso8601_time_regex, ISO8601_PATTERN);\n\n  AR_ISO_DATETIME_PATTERN =\n      (UChar*)\"\\\\A(?<year>\\\\d{4})-(?<month>\\\\d\\\\d)-(?<mday>\\\\d\\\\d) (?<hour>\\\\d\\\\d):(?<min>\\\\d\\\\d):(?<sec>\\\\d\\\\d)(\\\\.(?<microsec>\\\\d+))?\\\\z\";\n\n  build_regex(&ar_iso_datetime_regex, AR_ISO_DATETIME_PATTERN);\n}\n"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/type_cast/time_conversion.h",
    "content": "#pragma once\n\n#include <ctype.h>\n#include <ruby.h>\n#include <ruby/oniguruma.h>\n#include <stdbool.h>\n\nVALUE is_iso8601_time_string(const char* value);\nVALUE iso_ar_iso_datetime_string(const char* value);\nvoid panko_init_time(VALUE mPanko);\n"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/type_cast/type_cast.c",
    "content": "#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\nstatic VALUE oj_type = Qundef;\nstatic VALUE oj_parseerror_type = Qundef;\nID oj_sc_parse_id = 0;\n\n// Caching ActiveRecord Types\nstatic VALUE ar_string_type = Qundef;\nstatic VALUE ar_text_type = Qundef;\nstatic VALUE ar_float_type = Qundef;\nstatic VALUE ar_integer_type = Qundef;\nstatic VALUE ar_boolean_type = Qundef;\nstatic VALUE ar_date_time_type = Qundef;\nstatic VALUE ar_time_zone_converter = Qundef;\nstatic VALUE ar_json_type = Qundef;\n\nstatic VALUE ar_pg_integer_type = Qundef;\nstatic VALUE ar_pg_float_type = Qundef;\nstatic VALUE ar_pg_uuid_type = Qundef;\nstatic VALUE ar_pg_json_type = Qundef;\nstatic VALUE ar_pg_jsonb_type = Qundef;\nstatic VALUE ar_pg_array_type = Qundef;\nstatic VALUE ar_pg_date_time_type = Qundef;\nstatic VALUE ar_pg_timestamp_type = Qundef;\n\nstatic int initiailized = 0;\n\nVALUE cache_postgres_type_lookup(VALUE ar) {\n  VALUE ar_connection_adapters, ar_postgresql, ar_oid;\n\n  if (rb_const_defined_at(ar, rb_intern(\"ConnectionAdapters\")) != (int)Qtrue) {\n    return Qfalse;\n  }\n  ar_connection_adapters = rb_const_get_at(ar, rb_intern(\"ConnectionAdapters\"));\n\n  if (rb_const_defined_at(ar_connection_adapters, rb_intern(\"PostgreSQL\")) !=\n      (int)Qtrue) {\n    return Qfalse;\n  }\n  ar_postgresql =\n      rb_const_get_at(ar_connection_adapters, rb_intern(\"PostgreSQL\"));\n\n  if (rb_const_defined_at(ar_postgresql, rb_intern(\"OID\")) != (int)Qtrue) {\n    return Qfalse;\n  }\n  ar_oid = rb_const_get_at(ar_postgresql, rb_intern(\"OID\"));\n\n  if (rb_const_defined_at(ar_oid, rb_intern(\"Float\")) == (int)Qtrue) {\n    ar_pg_float_type = rb_const_get_at(ar_oid, rb_intern(\"Float\"));\n  }\n\n  if (rb_const_defined_at(ar_oid, rb_intern(\"Integer\")) == (int)Qtrue) {\n    ar_pg_integer_type = rb_const_get_at(ar_oid, rb_intern(\"Integer\"));\n  }\n\n  if (rb_const_defined_at(ar_oid, rb_intern(\"Uuid\")) == (int)Qtrue) {\n    ar_pg_uuid_type = rb_const_get_at(ar_oid, rb_intern(\"Uuid\"));\n  }\n\n  if (rb_const_defined_at(ar_oid, rb_intern(\"Json\")) == (int)Qtrue) {\n    ar_pg_json_type = rb_const_get_at(ar_oid, rb_intern(\"Json\"));\n  }\n\n  if (rb_const_defined_at(ar_oid, rb_intern(\"Jsonb\")) == (int)Qtrue) {\n    ar_pg_jsonb_type = rb_const_get_at(ar_oid, rb_intern(\"Jsonb\"));\n  }\n\n  if (rb_const_defined_at(ar_oid, rb_intern(\"DateTime\")) == (int)Qtrue) {\n    ar_pg_date_time_type = rb_const_get_at(ar_oid, rb_intern(\"DateTime\"));\n  }\n\n  if (rb_const_defined_at(ar_oid, rb_intern(\"Timestamp\")) == (int)Qtrue) {\n    ar_pg_timestamp_type = rb_const_get_at(ar_oid, rb_intern(\"Timestamp\"));\n  }\n\n  return Qtrue;\n}\n\nVALUE cache_time_zone_type_lookup(VALUE ar) {\n  VALUE ar_attr_methods, ar_time_zone_conversion;\n\n  // ActiveRecord::AttributeMethods\n  if (rb_const_defined_at(ar, rb_intern(\"AttributeMethods\")) != (int)Qtrue) {\n    return Qfalse;\n  }\n  ar_attr_methods = rb_const_get_at(ar, rb_intern(\"AttributeMethods\"));\n\n  // ActiveRecord::AttributeMethods::TimeZoneConversion\n  if (rb_const_defined_at(ar_attr_methods, rb_intern(\"TimeZoneConversion\")) !=\n      (int)Qtrue) {\n    return Qfalse;\n  }\n  ar_time_zone_conversion =\n      rb_const_get_at(ar_attr_methods, rb_intern(\"TimeZoneConversion\"));\n\n  if (rb_const_defined_at(ar_time_zone_conversion,\n                          rb_intern(\"TimeZoneConverter\")) != (int)Qtrue) {\n    return Qfalse;\n  }\n  ar_time_zone_converter =\n      rb_const_get_at(ar_time_zone_conversion, rb_intern(\"TimeZoneConverter\"));\n\n  return Qtrue;\n}\n\nvoid cache_type_lookup() {\n  if (initiailized == 1) {\n    return;\n  }\n\n  initiailized = 1;\n\n  VALUE ar, ar_type, ar_type_methods;\n\n  ar = rb_const_get_at(rb_cObject, rb_intern(\"ActiveRecord\"));\n\n  // ActiveRecord::Type\n  ar_type = rb_const_get_at(ar, rb_intern(\"Type\"));\n\n  ar_string_type = rb_const_get_at(ar_type, rb_intern(\"String\"));\n  ar_text_type = rb_const_get_at(ar_type, rb_intern(\"Text\"));\n  ar_float_type = rb_const_get_at(ar_type, rb_intern(\"Float\"));\n  ar_integer_type = rb_const_get_at(ar_type, rb_intern(\"Integer\"));\n  ar_boolean_type = rb_const_get_at(ar_type, rb_intern(\"Boolean\"));\n  ar_date_time_type = rb_const_get_at(ar_type, rb_intern(\"DateTime\"));\n\n  ar_type_methods = rb_class_instance_methods(0, NULL, ar_string_type);\n  if (rb_ary_includes(ar_type_methods,\n                      rb_to_symbol(rb_str_new_cstr(\"deserialize\")))) {\n    deserialize_from_db_id = rb_intern(\"deserialize\");\n  } else {\n    deserialize_from_db_id = rb_intern(\"type_cast_from_database\");\n  }\n\n  if (rb_const_defined_at(ar_type, rb_intern(\"Json\")) == (int)Qtrue) {\n    ar_json_type = rb_const_get_at(ar_type, rb_intern(\"Json\"));\n  }\n\n  int isErrored;\n  rb_protect(cache_postgres_type_lookup, ar, &isErrored);\n  if (isErrored) {\n    rb_set_errinfo(Qnil);\n  }\n\n  rb_protect(cache_time_zone_type_lookup, ar, &isErrored);\n  if (isErrored) {\n    rb_set_errinfo(Qnil);\n  }\n}\n\nbool is_string_or_text_type(VALUE type_klass) {\n  return type_klass == ar_string_type || type_klass == ar_text_type ||\n         (ar_pg_uuid_type != Qundef && type_klass == ar_pg_uuid_type);\n}\n\nVALUE cast_string_or_text_type(VALUE value) {\n  if (RB_TYPE_P(value, T_STRING)) {\n    return value;\n  }\n\n  if (value == Qtrue) {\n    return rb_str_new_cstr(\"t\");\n  }\n\n  if (value == Qfalse) {\n    return rb_str_new_cstr(\"f\");\n  }\n\n  return rb_funcall(value, to_s_id, 0);\n}\n\nbool is_float_type(VALUE type_klass) {\n  return type_klass == ar_float_type ||\n         (ar_pg_float_type != Qundef && type_klass == ar_pg_float_type);\n}\n\nVALUE cast_float_type(VALUE value) {\n  if (RB_TYPE_P(value, T_FLOAT)) {\n    return value;\n  }\n\n  if (RB_TYPE_P(value, T_STRING)) {\n    const char* val = StringValuePtr(value);\n    return rb_float_new(strtod(val, NULL));\n  }\n\n  return Qundef;\n}\n\nbool is_integer_type(VALUE type_klass) {\n  return type_klass == ar_integer_type ||\n         (ar_pg_integer_type != Qundef && type_klass == ar_pg_integer_type);\n}\n\nVALUE cast_integer_type(VALUE value) {\n  if (RB_INTEGER_TYPE_P(value)) {\n    return value;\n  }\n\n  if (RB_TYPE_P(value, T_STRING)) {\n    const char* val = StringValuePtr(value);\n    if (strlen(val) == 0) {\n      return Qnil;\n    }\n    return rb_cstr2inum(val, 10);\n  }\n\n  if (RB_FLOAT_TYPE_P(value)) {\n    // We are calling the `to_i` here, because ruby internal\n    // `flo_to_i` is not accessible\n    return rb_funcall(value, to_i_id, 0);\n  }\n\n  if (value == Qtrue) {\n    return INT2NUM(1);\n  }\n\n  if (value == Qfalse) {\n    return INT2NUM(0);\n  }\n\n  // At this point, we handled integer, float, string and booleans\n  // any thing other than this (array, hashes, etc) should result in nil\n  return Qnil;\n}\n\nbool is_json_type(VALUE type_klass) {\n  return ((ar_pg_json_type != Qundef && type_klass == ar_pg_json_type) ||\n          (ar_pg_jsonb_type != Qundef && type_klass == ar_pg_jsonb_type) ||\n          (ar_json_type != Qundef && type_klass == ar_json_type));\n}\n\nbool is_boolean_type(VALUE type_klass) { return type_klass == ar_boolean_type; }\n\nVALUE cast_boolean_type(VALUE value) {\n  if (value == Qtrue || value == Qfalse) {\n    return value;\n  }\n\n  if (value == Qnil) {\n    return Qnil;\n  }\n\n  if (RB_TYPE_P(value, T_STRING)) {\n    if (RSTRING_LEN(value) == 0) {\n      return Qnil;\n    }\n\n    const char* val = StringValuePtr(value);\n\n    bool isFalseValue =\n        (*val == '0' || (*val == 'f' || *val == 'F') ||\n         (strcmp(val, \"false\") == 0 || strcmp(val, \"FALSE\") == 0) ||\n         (strcmp(val, \"off\") == 0 || strcmp(val, \"OFF\") == 0));\n\n    return isFalseValue ? Qfalse : Qtrue;\n  }\n\n  if (RB_INTEGER_TYPE_P(value)) {\n    return value == INT2NUM(1) ? Qtrue : Qfalse;\n  }\n\n  return Qundef;\n}\n\nbool is_date_time_type(VALUE type_klass) {\n  return (type_klass == ar_date_time_type) ||\n         (ar_pg_date_time_type != Qundef &&\n          type_klass == ar_pg_date_time_type) ||\n         (ar_pg_timestamp_type != Qundef &&\n          type_klass == ar_pg_timestamp_type) ||\n         (ar_time_zone_converter != Qundef &&\n          type_klass == ar_time_zone_converter);\n}\n\nVALUE cast_date_time_type(VALUE value) {\n  // Instead of take strings to comparing them to time zones\n  // and then comparing them back to string\n  // We will just make sure we have string on ISO8601 and it's utc\n  if (RB_TYPE_P(value, T_STRING)) {\n    const char* val = StringValuePtr(value);\n    // 'Z' in ISO8601 says it's UTC\n    if (val[strlen(val) - 1] == 'Z' && is_iso8601_time_string(val) == Qtrue) {\n      return value;\n    }\n\n    volatile VALUE iso8601_string = iso_ar_iso_datetime_string(val);\n    if (iso8601_string != Qnil) {\n      return iso8601_string;\n    }\n  }\n\n  return Qundef;\n}\n\nVALUE rescue_func(VALUE _arg, VALUE _data) { return Qfalse; }\n\nVALUE parse_json(VALUE value) {\n  return rb_funcall(oj_type, oj_sc_parse_id, 2, rb_cObject, value);\n}\n\nVALUE is_json_value(VALUE value) {\n  if (!RB_TYPE_P(value, T_STRING)) {\n    return value;\n  }\n\n  if (RSTRING_LEN(value) == 0) {\n    return Qfalse;\n  }\n\n  volatile VALUE result =\n      rb_rescue2(parse_json, value, rescue_func, Qundef, oj_parseerror_type, 0);\n\n  if (NIL_P(result)) {\n    return Qtrue;\n  }\n\n  if (result == Qfalse) {\n    return Qfalse;\n  }\n\n  // TODO: fix me!\n  return Qfalse;\n}\n\nVALUE type_cast(VALUE type_metadata, VALUE value, volatile VALUE* isJson) {\n  if (value == Qnil || value == Qundef) {\n    return value;\n  }\n\n  cache_type_lookup();\n\n  VALUE type_klass, typeCastedValue;\n\n  type_klass = CLASS_OF(type_metadata);\n  typeCastedValue = Qundef;\n\n  TypeCast typeCast;\n  for (typeCast = type_casts; typeCast->canCast != NULL; typeCast++) {\n    if (typeCast->canCast(type_klass)) {\n      typeCastedValue = typeCast->typeCast(value);\n      break;\n    }\n  }\n\n  if (is_json_type(type_klass)) {\n    if (is_json_value(value) == Qfalse) {\n      return Qnil;\n    }\n    *isJson = Qtrue;\n    return value;\n  }\n\n  if (typeCastedValue == Qundef) {\n    return rb_funcall(type_metadata, deserialize_from_db_id, 1, value);\n  }\n\n  return typeCastedValue;\n}\n\nVALUE public_type_cast(int argc, VALUE* argv, VALUE self) {\n  VALUE type_metadata, value, isJson;\n  rb_scan_args(argc, argv, \"21\", &type_metadata, &value, &isJson);\n\n  if (isJson == Qnil || isJson == Qundef) {\n    isJson = Qfalse;\n  }\n\n  return type_cast(type_metadata, value, &isJson);\n}\n\nvoid panko_init_type_cast(VALUE mPanko) {\n  to_s_id = rb_intern(\"to_s\");\n  to_i_id = rb_intern(\"to_i\");\n\n  oj_type = rb_const_get_at(rb_cObject, rb_intern(\"Oj\"));\n  oj_parseerror_type = rb_const_get_at(oj_type, rb_intern(\"ParseError\"));\n  oj_sc_parse_id = rb_intern(\"sc_parse\");\n\n  // TODO: pass 3 arguments here\n  rb_define_singleton_method(mPanko, \"_type_cast\", public_type_cast, -1);\n\n  panko_init_time(mPanko);\n\n  rb_global_variable(&oj_type);\n  rb_global_variable(&oj_parseerror_type);\n  rb_global_variable(&ar_string_type);\n  rb_global_variable(&ar_text_type);\n  rb_global_variable(&ar_float_type);\n  rb_global_variable(&ar_integer_type);\n  rb_global_variable(&ar_boolean_type);\n  rb_global_variable(&ar_date_time_type);\n  rb_global_variable(&ar_time_zone_converter);\n  rb_global_variable(&ar_json_type);\n  rb_global_variable(&ar_pg_integer_type);\n  rb_global_variable(&ar_pg_float_type);\n  rb_global_variable(&ar_pg_uuid_type);\n  rb_global_variable(&ar_pg_json_type);\n  rb_global_variable(&ar_pg_jsonb_type);\n  rb_global_variable(&ar_pg_array_type);\n  rb_global_variable(&ar_pg_date_time_type);\n  rb_global_variable(&ar_pg_timestamp_type);\n}\n"
  },
  {
    "path": "ext/panko_serializer/attributes_writer/type_cast/type_cast.h",
    "content": "#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 of two inspirations:\n *  *) light records gem\n *  *) pg TextDecoders\n *\n * The whole idea behind those type casts, are to do the minimum required\n * type casting in the most performant manner and *allocation free*.\n *\n * For example, in `ActiveRecord::Type::String` the type_cast_from_database\n * creates new string, for known reasons, but, in serialization flow we don't\n * need to create new string becuase we afraid of mutations.\n *\n * Since we know before hand, that we are only reading from the database, and\n * *not* writing and the end result if for JSON we can skip some \"defenses\".\n */\n\ntypedef bool (*TypeMatchFunc)(VALUE type_klass);\n\n/*\n * TypeCastFunc\n *\n * @return VALUE casted value or Qundef if not casted\n */\ntypedef VALUE (*TypeCastFunc)(VALUE value);\n\ntypedef struct _TypeCast {\n  TypeMatchFunc canCast;\n  TypeCastFunc typeCast;\n}* TypeCast;\n\n// ActiveRecord::Type::String\n// ActiveRecord::Type::Text\nbool is_string_or_text_type(VALUE type_klass);\nVALUE cast_string_or_text_type(VALUE value);\n\n// ActiveRecord::Type::Float\nbool is_float_type(VALUE type_klass);\nVALUE cast_float_type(VALUE value);\n\n// ActiveRecord::Type::Integer\nbool is_integer_type(VALUE type_klass);\nVALUE cast_integer_type(VALUE value);\n\n// ActiveRecord::ConnectoinAdapters::PostgreSQL::Json\nbool is_json_type(VALUE type_klass);\nVALUE cast_json_type(VALUE value);\n\n// ActiveRecord::Type::Boolean\nbool is_boolean_type(VALUE type_klass);\nVALUE cast_boolean_type(VALUE value);\n\n// ActiveRecord::Type::DateTime\n// ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime\n// ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter\nbool is_date_time_type(VALUE type_klass);\nVALUE cast_date_time_type(VALUE value);\n\nstatic struct _TypeCast type_casts[] = {\n    {is_string_or_text_type, cast_string_or_text_type},\n    {is_integer_type, cast_integer_type},\n    {is_boolean_type, cast_boolean_type},\n    {is_date_time_type, cast_date_time_type},\n    {is_float_type, cast_float_type},\n\n    {NULL, NULL}};\n\nextern VALUE type_cast(VALUE type_metadata, VALUE value,\n                       volatile VALUE* isJson);\nvoid panko_init_type_cast(VALUE mPanko);\n\n// Introduced in ruby 2.4\n#ifndef RB_INTEGER_TYPE_P\n#define RB_INTEGER_TYPE_P(obj) (RB_FIXNUM_P(obj) || RB_TYPE_P(obj, T_BIGNUM))\n#endif\n"
  },
  {
    "path": "ext/panko_serializer/common.h",
    "content": "#pragma once\n\n#include <ruby.h>\n\n#define PANKO_SAFE_HASH_SIZE(hash) \\\n  (hash == Qnil || hash == Qundef) ? 0 : RHASH_SIZE(hash)\n\n#define PANKO_EMPTY_HASH(hash) \\\n  (hash == Qnil || hash == Qundef) ? 1 : (RHASH_SIZE(hash) == 0)\n"
  },
  {
    "path": "ext/panko_serializer/extconf.rb",
    "content": "# frozen_string_literal: true\nrequire \"mkmf\"\nrequire \"pathname\"\n\n$CPPFLAGS += \" -Wall\"\n\nextension_name = \"panko_serializer\"\ndir_config(extension_name)\n\nRbConfig.expand(srcdir = \"$(srcdir)\".dup)\n\n# enum all source files\n$srcs = Dir[File.join(srcdir, \"**/*.c\")]\n\n\n# Get all source directories recursivley\ndirectories = Dir[File.join(srcdir, \"**/*\")].select { |f| File.directory?(f) }\ndirectories = directories.map { |d| Pathname.new(d).relative_path_from(Pathname.new(srcdir)) }\ndirectories.each do |dir|\n\t# add include path to the internal folder\n\t# $(srcdir) is a root folder, where \"extconf.rb\" is stored\n\t$INCFLAGS << \" -I$(srcdir)/#{dir}\"\n\n\t# add folder, where compiler can search source files\n\t$VPATH << \"$(srcdir)/#{dir}\"\nend\n\ncreate_makefile(\"panko/panko_serializer\")\n"
  },
  {
    "path": "ext/panko_serializer/panko_serializer.c",
    "content": "#include \"panko_serializer.h\"\n\n#include <ruby.h>\n\nstatic ID push_value_id;\nstatic ID push_array_id;\nstatic ID push_object_id;\nstatic ID push_json_id;\nstatic ID pop_id;\n\nstatic ID to_a_id;\n\nstatic ID object_id;\nstatic ID serialization_context_id;\n\nstatic VALUE SKIP = Qundef;\n\nvoid write_value(VALUE str_writer, VALUE key, VALUE value, VALUE isJson) {\n  if (isJson == Qtrue) {\n    rb_funcall(str_writer, push_json_id, 2, value, key);\n  } else {\n    rb_funcall(str_writer, push_value_id, 2, value, key);\n  }\n}\n\nvoid serialize_method_fields(VALUE object, VALUE str_writer,\n                             SerializationDescriptor descriptor) {\n  if (RARRAY_LEN(descriptor->method_fields) == 0) {\n    return;\n  }\n\n  volatile VALUE method_fields, serializer, key;\n  long i;\n\n  method_fields = descriptor->method_fields;\n\n  serializer = descriptor->serializer;\n  rb_ivar_set(serializer, object_id, object);\n\n  for (i = 0; i < RARRAY_LEN(method_fields); i++) {\n    volatile VALUE raw_attribute = RARRAY_AREF(method_fields, i);\n    Attribute attribute = PANKO_ATTRIBUTE_READ(raw_attribute);\n\n    volatile VALUE result = rb_funcall(serializer, attribute->name_id, 0);\n    if (result != SKIP) {\n      key = attr_name_for_serialization(attribute);\n      write_value(str_writer, key, result, Qfalse);\n    }\n  }\n\n  rb_ivar_set(serializer, object_id, Qnil);\n}\n\nvoid serialize_fields(VALUE object, VALUE str_writer,\n                      SerializationDescriptor descriptor) {\n  descriptor->attributes_writer.write_attributes(object, descriptor->attributes,\n                                                 write_value, str_writer);\n\n  serialize_method_fields(object, str_writer, descriptor);\n}\n\nvoid serialize_has_one_associations(VALUE object, VALUE str_writer,\n                                    VALUE associations) {\n  long i;\n  for (i = 0; i < RARRAY_LEN(associations); i++) {\n    volatile VALUE association_el = RARRAY_AREF(associations, i);\n    Association association = association_read(association_el);\n\n    volatile VALUE value = rb_funcall(object, association->name_id, 0);\n\n    if (NIL_P(value)) {\n      write_value(str_writer, association->name_str, value, Qfalse);\n    } else {\n      serialize_object(association->name_str, value, str_writer,\n                       association->descriptor);\n    }\n  }\n}\n\nvoid serialize_has_many_associations(VALUE object, VALUE str_writer,\n                                     VALUE associations) {\n  long i;\n  for (i = 0; i < RARRAY_LEN(associations); i++) {\n    volatile VALUE association_el = RARRAY_AREF(associations, i);\n    Association association = association_read(association_el);\n\n    volatile VALUE value = rb_funcall(object, association->name_id, 0);\n\n    if (NIL_P(value)) {\n      write_value(str_writer, association->name_str, value, Qfalse);\n    } else {\n      serialize_objects(association->name_str, value, str_writer,\n                        association->descriptor);\n    }\n  }\n}\n\nVALUE serialize_object(VALUE key, VALUE object, VALUE str_writer,\n                       SerializationDescriptor descriptor) {\n  sd_set_writer(descriptor, object);\n\n  rb_funcall(str_writer, push_object_id, 1, key);\n\n  serialize_fields(object, str_writer, descriptor);\n\n  if (RARRAY_LEN(descriptor->has_one_associations) > 0) {\n    serialize_has_one_associations(object, str_writer,\n                                   descriptor->has_one_associations);\n  }\n\n  if (RARRAY_LEN(descriptor->has_many_associations) > 0) {\n    serialize_has_many_associations(object, str_writer,\n                                    descriptor->has_many_associations);\n  }\n\n  rb_funcall(str_writer, pop_id, 0);\n\n  return Qnil;\n}\n\nVALUE serialize_objects(VALUE key, VALUE objects, VALUE str_writer,\n                        SerializationDescriptor descriptor) {\n  long i;\n\n  rb_funcall(str_writer, push_array_id, 1, key);\n\n  if (!RB_TYPE_P(objects, T_ARRAY)) {\n    objects = rb_funcall(objects, to_a_id, 0);\n  }\n\n  for (i = 0; i < RARRAY_LEN(objects); i++) {\n    volatile VALUE object = RARRAY_AREF(objects, i);\n    serialize_object(Qnil, object, str_writer, descriptor);\n  }\n\n  rb_funcall(str_writer, pop_id, 0);\n\n  return Qnil;\n}\n\nVALUE serialize_object_api(VALUE klass, VALUE object, VALUE str_writer,\n                           VALUE descriptor) {\n  SerializationDescriptor sd = sd_read(descriptor);\n  return serialize_object(Qnil, object, str_writer, sd);\n}\n\nVALUE serialize_objects_api(VALUE klass, VALUE objects, VALUE str_writer,\n                            VALUE descriptor) {\n  serialize_objects(Qnil, objects, str_writer, sd_read(descriptor));\n\n  return Qnil;\n}\n\nvoid Init_panko_serializer() {\n  push_value_id = rb_intern(\"push_value\");\n  push_array_id = rb_intern(\"push_array\");\n  push_object_id = rb_intern(\"push_object\");\n  push_json_id = rb_intern(\"push_json\");\n  pop_id = rb_intern(\"pop\");\n  to_a_id = rb_intern(\"to_a\");\n  object_id = rb_intern(\"@object\");\n  serialization_context_id = rb_intern(\"@serialization_context\");\n\n  VALUE mPanko = rb_define_module(\"Panko\");\n\n  rb_define_singleton_method(mPanko, \"serialize_object\", serialize_object_api,\n                             3);\n\n  rb_define_singleton_method(mPanko, \"serialize_objects\", serialize_objects_api,\n                             3);\n\n  VALUE mPankoSerializer = rb_const_get(mPanko, rb_intern(\"Serializer\"));\n  SKIP = rb_const_get(mPankoSerializer, rb_intern(\"SKIP\"));\n  rb_global_variable(&SKIP);\n\n  panko_init_serialization_descriptor(mPanko);\n  init_attributes_writer(mPanko);\n  panko_init_type_cast(mPanko);\n  panko_init_attribute(mPanko);\n  panko_init_association(mPanko);\n}\n"
  },
  {
    "path": "ext/panko_serializer/panko_serializer.h",
    "content": "#include <ruby.h>\n\n#include \"attributes_writer/attributes_writer.h\"\n#include \"serialization_descriptor/association.h\"\n#include \"serialization_descriptor/attribute.h\"\n#include \"serialization_descriptor/serialization_descriptor.h\"\n\nVALUE serialize_object(VALUE key, VALUE object, VALUE str_writer,\n                       SerializationDescriptor descriptor);\n\nVALUE serialize_objects(VALUE key, VALUE objects, VALUE str_writer,\n                        SerializationDescriptor descriptor);\n"
  },
  {
    "path": "ext/panko_serializer/serialization_descriptor/association.c",
    "content": "#include \"association.h\"\n\nVALUE cAssociation;\n\nstatic void association_free(void* ptr) {\n  if (!ptr) {\n    return;\n  }\n\n  Association association = (Association)ptr;\n  association->name_str = Qnil;\n  association->name_id = 0;\n  association->name_sym = Qnil;\n  association->rb_descriptor = Qnil;\n\n  if (!association->descriptor || association->descriptor != NULL) {\n    association->descriptor = NULL;\n  }\n\n  xfree(association);\n}\n\nvoid association_mark(Association data) {\n  rb_gc_mark(data->name_str);\n  rb_gc_mark(data->name_sym);\n  rb_gc_mark(data->rb_descriptor);\n\n  if (data->descriptor != NULL) {\n    sd_mark(data->descriptor);\n  }\n}\n\nstatic VALUE association_new(int argc, VALUE* argv, VALUE self) {\n  Association association;\n\n  Check_Type(argv[0], T_SYMBOL);\n  Check_Type(argv[1], T_STRING);\n\n  association = ALLOC(struct _Association);\n  association->name_sym = argv[0];\n  association->name_str = argv[1];\n  association->rb_descriptor = argv[2];\n\n  association->name_id = rb_intern_str(rb_sym2str(association->name_sym));\n  association->descriptor = sd_read(association->rb_descriptor);\n\n  return Data_Wrap_Struct(cAssociation, association_mark, association_free,\n                          association);\n}\n\nAssociation association_read(VALUE association) {\n  return (Association)DATA_PTR(association);\n}\n\nVALUE association_name_sym_ref(VALUE self) {\n  Association association = (Association)DATA_PTR(self);\n  return association->name_sym;\n}\n\nVALUE association_name_str_ref(VALUE self) {\n  Association association = (Association)DATA_PTR(self);\n  return association->name_str;\n}\n\nVALUE association_descriptor_ref(VALUE self) {\n  Association association = (Association)DATA_PTR(self);\n  return association->rb_descriptor;\n}\n\nVALUE association_decriptor_aset(VALUE self, VALUE descriptor) {\n  Association association = (Association)DATA_PTR(self);\n\n  association->rb_descriptor = descriptor;\n  association->descriptor = sd_read(descriptor);\n\n  return association->rb_descriptor;\n}\n\nvoid panko_init_association(VALUE mPanko) {\n  cAssociation = rb_define_class_under(mPanko, \"Association\", rb_cObject);\n  rb_undef_alloc_func(cAssociation);\n  rb_global_variable(&cAssociation);\n\n  rb_define_module_function(cAssociation, \"new\", association_new, -1);\n\n  rb_define_method(cAssociation, \"name_sym\", association_name_sym_ref, 0);\n  rb_define_method(cAssociation, \"name_str\", association_name_str_ref, 0);\n  rb_define_method(cAssociation, \"descriptor\", association_descriptor_ref, 0);\n  rb_define_method(cAssociation, \"descriptor=\", association_decriptor_aset, 1);\n}\n"
  },
  {
    "path": "ext/panko_serializer/serialization_descriptor/association.h",
    "content": "#include <ruby.h>\n\n#ifndef __ASSOCIATION_H__\n#define __ASSOCIATION_H__\n\n#include \"serialization_descriptor.h\"\n\ntypedef struct _Association {\n  ID name_id;\n  VALUE name_sym;\n  VALUE name_str;\n\n  VALUE rb_descriptor;\n  SerializationDescriptor descriptor;\n}* Association;\n\nAssociation association_read(VALUE association);\nvoid panko_init_association(VALUE mPanko);\n\n#endif\n"
  },
  {
    "path": "ext/panko_serializer/serialization_descriptor/attribute.c",
    "content": "#include \"attribute.h\"\n\nID attribute_aliases_id = 0;\nVALUE cAttribute;\n\nstatic void attribute_free(void* ptr) {\n  if (!ptr) {\n    return;\n  }\n\n  Attribute attribute = (Attribute)ptr;\n  attribute->name_str = Qnil;\n  attribute->name_id = 0;\n  attribute->alias_name = Qnil;\n  attribute->type = Qnil;\n  attribute->record_class = Qnil;\n\n  xfree(attribute);\n}\n\nvoid attribute_mark(Attribute data) {\n  rb_gc_mark(data->name_str);\n  rb_gc_mark(data->alias_name);\n  rb_gc_mark(data->type);\n  rb_gc_mark(data->record_class);\n}\n\nstatic VALUE attribute_new(int argc, VALUE* argv, VALUE self) {\n  Attribute attribute;\n\n  Check_Type(argv[0], T_STRING);\n  if (argv[1] != Qnil) {\n    Check_Type(argv[1], T_STRING);\n  }\n\n  attribute = ALLOC(struct _Attribute);\n  attribute->name_str = argv[0];\n  attribute->name_id = rb_intern_str(attribute->name_str);\n  attribute->alias_name = argv[1];\n  attribute->type = Qnil;\n  attribute->record_class = Qnil;\n\n  return Data_Wrap_Struct(cAttribute, attribute_mark, attribute_free,\n                          attribute);\n}\n\nAttribute attribute_read(VALUE attribute) {\n  return (Attribute)DATA_PTR(attribute);\n}\n\nvoid attribute_try_invalidate(Attribute attribute, VALUE new_record_class) {\n  if (attribute->record_class != new_record_class) {\n    attribute->type = Qnil;\n    attribute->record_class = new_record_class;\n\n    // Once the record class is changed for this attribute, check if\n    // we attribute_aliases (from ActivRecord), if so fill in\n    // performance wise - this code should be called once (unless the serialzier\n    // is polymorphic)\n    volatile VALUE ar_aliases_hash =\n        rb_funcall(new_record_class, attribute_aliases_id, 0);\n\n    if (!PANKO_EMPTY_HASH(ar_aliases_hash)) {\n      volatile VALUE aliasedValue =\n          rb_hash_aref(ar_aliases_hash, attribute->name_str);\n      if (aliasedValue != Qnil) {\n        attribute->alias_name = attribute->name_str;\n        attribute->name_str = aliasedValue;\n        attribute->name_id = rb_intern_str(attribute->name_str);\n      }\n    }\n  }\n}\n\nVALUE attribute_name_ref(VALUE self) {\n  Attribute attribute = (Attribute)DATA_PTR(self);\n  return attribute->name_str;\n}\n\nVALUE attribute_alias_name_ref(VALUE self) {\n  Attribute attribute = (Attribute)DATA_PTR(self);\n  return attribute->alias_name;\n}\n\nvoid panko_init_attribute(VALUE mPanko) {\n  attribute_aliases_id = rb_intern(\"attribute_aliases\");\n\n  cAttribute = rb_define_class_under(mPanko, \"Attribute\", rb_cObject);\n  rb_undef_alloc_func(cAttribute);\n  rb_global_variable(&cAttribute);\n\n  rb_define_module_function(cAttribute, \"new\", attribute_new, -1);\n\n  rb_define_method(cAttribute, \"name\", attribute_name_ref, 0);\n  rb_define_method(cAttribute, \"alias_name\", attribute_alias_name_ref, 0);\n}\n"
  },
  {
    "path": "ext/panko_serializer/serialization_descriptor/attribute.h",
    "content": "#include <ruby.h>\n\n#ifndef __ATTRIBUTE_H__\n#define __ATTRIBUTE_H__\n\n#include \"../common.h\"\n\ntypedef struct _Attribute {\n  VALUE name_str;\n  ID name_id;\n  VALUE alias_name;\n\n  /*\n   * We will cache the activerecord type\n   * by the record_class\n   */\n  VALUE type;\n  VALUE record_class;\n}* Attribute;\n\nAttribute attribute_read(VALUE attribute);\nvoid attribute_try_invalidate(Attribute attribute, VALUE new_record_class);\nvoid panko_init_attribute(VALUE mPanko);\n\n#define PANKO_ATTRIBUTE_READ(attribute) (Attribute) DATA_PTR(attribute)\n\n#endif\n"
  },
  {
    "path": "ext/panko_serializer/serialization_descriptor/serialization_descriptor.c",
    "content": "#include \"serialization_descriptor.h\"\n\nstatic ID object_id;\nstatic ID sc_id;\n\nstatic void sd_free(SerializationDescriptor sd) {\n  if (!sd) {\n    return;\n  }\n\n  sd->serializer = Qnil;\n  sd->serializer_type = Qnil;\n  sd->attributes = Qnil;\n  sd->method_fields = Qnil;\n  sd->has_one_associations = Qnil;\n  sd->has_many_associations = Qnil;\n  sd->aliases = Qnil;\n  xfree(sd);\n}\n\nvoid sd_mark(SerializationDescriptor data) {\n  rb_gc_mark(data->serializer);\n  rb_gc_mark(data->serializer_type);\n  rb_gc_mark(data->attributes);\n  rb_gc_mark(data->method_fields);\n  rb_gc_mark(data->has_one_associations);\n  rb_gc_mark(data->has_many_associations);\n  rb_gc_mark(data->aliases);\n}\n\nstatic VALUE sd_alloc(VALUE klass) {\n  SerializationDescriptor sd = ALLOC(struct _SerializationDescriptor);\n\n  sd->serializer = Qnil;\n  sd->serializer_type = Qnil;\n  sd->attributes = Qnil;\n  sd->method_fields = Qnil;\n  sd->has_one_associations = Qnil;\n  sd->has_many_associations = Qnil;\n  sd->aliases = Qnil;\n\n  sd->attributes_writer = create_empty_attributes_writer();\n\n  return Data_Wrap_Struct(klass, sd_mark, sd_free, sd);\n}\n\nSerializationDescriptor sd_read(VALUE descriptor) {\n  return (SerializationDescriptor)DATA_PTR(descriptor);\n}\n\nvoid sd_set_writer(SerializationDescriptor sd, VALUE object) {\n  if (sd->attributes_writer.object_type != UnknownObjectType) {\n    return;\n  }\n\n  sd->attributes_writer = create_attributes_writer(object);\n}\n\nVALUE sd_serializer_set(VALUE self, VALUE serializer) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n\n  sd->serializer = serializer;\n  return Qnil;\n}\n\nVALUE sd_serializer_ref(VALUE self) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n\n  return sd->serializer;\n}\n\nVALUE sd_attributes_set(VALUE self, VALUE attributes) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n\n  sd->attributes = attributes;\n  return Qnil;\n}\n\nVALUE sd_attributes_ref(VALUE self) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n  return sd->attributes;\n}\n\nVALUE sd_method_fields_set(VALUE self, VALUE method_fields) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n  sd->method_fields = method_fields;\n  return Qnil;\n}\n\nVALUE sd_method_fields_ref(VALUE self) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n  return sd->method_fields;\n}\n\nVALUE sd_has_one_associations_set(VALUE self, VALUE has_one_associations) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n  sd->has_one_associations = has_one_associations;\n  return Qnil;\n}\n\nVALUE sd_has_one_associations_ref(VALUE self) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n  return sd->has_one_associations;\n}\n\nVALUE sd_has_many_associations_set(VALUE self, VALUE has_many_associations) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n  sd->has_many_associations = has_many_associations;\n  return Qnil;\n}\n\nVALUE sd_has_many_associations_ref(VALUE self) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n  return sd->has_many_associations;\n}\n\nVALUE sd_type_set(VALUE self, VALUE type) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n  sd->serializer_type = type;\n  return Qnil;\n}\n\nVALUE sd_type_aref(VALUE self) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n  return sd->serializer_type;\n}\n\nVALUE sd_aliases_set(VALUE self, VALUE aliases) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n  sd->aliases = aliases;\n  return Qnil;\n}\n\nVALUE sd_aliases_aref(VALUE self) {\n  SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);\n  return sd->aliases;\n}\n\nvoid panko_init_serialization_descriptor(VALUE mPanko) {\n  object_id = rb_intern(\"@object\");\n  sc_id = rb_intern(\"@sc\");\n\n  VALUE cSerializationDescriptor =\n      rb_define_class_under(mPanko, \"SerializationDescriptor\", rb_cObject);\n\n  rb_define_alloc_func(cSerializationDescriptor, sd_alloc);\n  rb_define_method(cSerializationDescriptor, \"serializer=\", sd_serializer_set,\n                   1);\n  rb_define_method(cSerializationDescriptor, \"serializer\", sd_serializer_ref,\n                   0);\n\n  rb_define_method(cSerializationDescriptor, \"attributes=\", sd_attributes_set,\n                   1);\n  rb_define_method(cSerializationDescriptor, \"attributes\", sd_attributes_ref,\n                   0);\n\n  rb_define_method(cSerializationDescriptor,\n                   \"method_fields=\", sd_method_fields_set, 1);\n  rb_define_method(cSerializationDescriptor, \"method_fields\",\n                   sd_method_fields_ref, 0);\n\n  rb_define_method(cSerializationDescriptor,\n                   \"has_one_associations=\", sd_has_one_associations_set, 1);\n  rb_define_method(cSerializationDescriptor, \"has_one_associations\",\n                   sd_has_one_associations_ref, 0);\n\n  rb_define_method(cSerializationDescriptor,\n                   \"has_many_associations=\", sd_has_many_associations_set, 1);\n  rb_define_method(cSerializationDescriptor, \"has_many_associations\",\n                   sd_has_many_associations_ref, 0);\n\n  rb_define_method(cSerializationDescriptor, \"type=\", sd_type_set, 1);\n  rb_define_method(cSerializationDescriptor, \"type\", sd_type_aref, 0);\n\n  rb_define_method(cSerializationDescriptor, \"aliases=\", sd_aliases_set, 1);\n  rb_define_method(cSerializationDescriptor, \"aliases\", sd_aliases_aref, 0);\n}\n"
  },
  {
    "path": "ext/panko_serializer/serialization_descriptor/serialization_descriptor.h",
    "content": "#pragma once\n\n#include <ruby.h>\n#include <stdbool.h>\n\n#include \"attributes_writer/attributes_writer.h\"\n\ntypedef struct _SerializationDescriptor {\n  // type of the serializer, so we can create it later\n  VALUE serializer_type;\n  // Cached value of the serializer\n  VALUE serializer;\n\n  // Metadata\n  VALUE attributes;\n  VALUE aliases;\n  VALUE method_fields;\n  VALUE has_one_associations;\n  VALUE has_many_associations;\n\n  AttributesWriter attributes_writer;\n}* SerializationDescriptor;\n\nSerializationDescriptor sd_read(VALUE descriptor);\n\nvoid sd_mark(SerializationDescriptor data);\n\nvoid sd_set_writer(SerializationDescriptor sd, VALUE object);\n\nvoid panko_init_serialization_descriptor(VALUE mPanko);\n"
  },
  {
    "path": "gemfiles/7.2.0.gemfile",
    "content": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activesupport\", \"~> 7.2.0\"\ngem \"activemodel\", \"~> 7.2.0\"\ngem \"activerecord\", \"~> 7.2.0\", group: :test\ngem \"trilogy\"\ngem \"sqlite3\", \"~> 1.4\"\n\ngroup :benchmarks do\n  gem \"vernier\"\n  gem \"stackprof\"\n  gem \"pg\"\n  gem \"benchmark-ips\"\n  gem \"memory_profiler\"\nend\n\ngroup :test do\n  gem \"faker\"\n  gem \"temping\"\nend\n\ngroup :development do\n  gem \"byebug\"\n  gem \"rake\"\n  gem \"rspec\", \"~> 3.0\"\n  gem \"rake-compiler\"\nend\n\ngroup :development, :test do\n  gem \"rubocop\"\n  gem \"standard\"\n  gem \"standard-performance\"\n  gem \"rubocop-performance\"\n  gem \"rubocop-rspec\"\nend\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/8.0.0.gemfile",
    "content": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activesupport\", \"~> 8.0.0\"\ngem \"activemodel\", \"~> 8.0.0\"\ngem \"activerecord\", \"~> 8.0.0\", group: :test\ngem \"trilogy\"\ngem \"sqlite3\", \">= 2.1\"\n\ngroup :benchmarks do\n  gem \"vernier\"\n  gem \"stackprof\"\n  gem \"pg\"\n  gem \"benchmark-ips\"\n  gem \"memory_profiler\"\nend\n\ngroup :test do\n  gem \"faker\"\n  gem \"temping\"\nend\n\ngroup :development do\n  gem \"byebug\"\n  gem \"rake\"\n  gem \"rspec\", \"~> 3.0\"\n  gem \"rake-compiler\"\nend\n\ngroup :development, :test do\n  gem \"rubocop\"\n  gem \"standard\"\n  gem \"standard-performance\"\n  gem \"rubocop-performance\"\n  gem \"rubocop-rspec\"\nend\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/8.1.0.gemfile",
    "content": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activesupport\", \"~> 8.1.0\"\ngem \"activemodel\", \"~> 8.1.0\"\ngem \"activerecord\", \"~> 8.1.0\", group: :test\ngem \"trilogy\"\ngem \"sqlite3\", \">= 2.1\"\n\ngroup :benchmarks do\n  gem \"vernier\"\n  gem \"stackprof\"\n  gem \"pg\"\n  gem \"benchmark-ips\"\n  gem \"memory_profiler\"\nend\n\ngroup :test do\n  gem \"faker\"\n  gem \"temping\"\nend\n\ngroup :development do\n  gem \"byebug\"\n  gem \"rake\"\n  gem \"rspec\", \"~> 3.0\"\n  gem \"rake-compiler\"\nend\n\ngroup :development, :test do\n  gem \"rubocop\"\n  gem \"standard\"\n  gem \"standard-performance\"\n  gem \"rubocop-performance\"\n  gem \"rubocop-rspec\"\nend\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "lib/panko/array_serializer.rb",
    "content": "# frozen_string_literal: true\n\nmodule Panko\n  class ArraySerializer\n    attr_accessor :subjects\n\n    def initialize(subjects, options = {})\n      @subjects = subjects\n      @each_serializer = options[:each_serializer]\n\n      if @each_serializer.nil?\n        raise ArgumentError, %{\nPlease pass valid each_serializer to ArraySerializer, for example:\n> Panko::ArraySerializer.new(posts, each_serializer: PostSerializer)\n        }\n      end\n\n      serializer_options = {\n        only: options.fetch(:only, []),\n        except: options.fetch(:except, []),\n        context: options[:context],\n        scope: options[:scope]\n      }\n\n      @serialization_context = SerializationContext.create(options)\n      @descriptor = Panko::SerializationDescriptor.build(@each_serializer, serializer_options, @serialization_context)\n    end\n\n    def to_json\n      serialize_to_json @subjects\n    end\n\n    def serialize(subjects)\n      serialize_with_writer(subjects, Panko::ObjectWriter.new).output\n    end\n\n    def to_a\n      serialize_with_writer(@subjects, Panko::ObjectWriter.new).output\n    end\n\n    def serialize_to_json(subjects)\n      serialize_with_writer(subjects, Oj::StringWriter.new(mode: :rails)).to_s\n    end\n\n    private\n\n    def serialize_with_writer(subjects, writer)\n      Panko.serialize_objects(subjects.to_a, writer, @descriptor)\n      writer\n    end\n  end\nend\n"
  },
  {
    "path": "lib/panko/association.rb",
    "content": "# frozen_string_literal: true\n\nmodule Panko\n  class Association\n    def duplicate\n      Panko::Association.new(\n        name_sym,\n        name_str,\n        Panko::SerializationDescriptor.duplicate(descriptor)\n      )\n    end\n\n    def inspect\n      \"<Panko::Association name=#{name_str.inspect}>\"\n    end\n  end\nend\n"
  },
  {
    "path": "lib/panko/attribute.rb",
    "content": "# frozen_string_literal: true\n\nmodule Panko\n  class Attribute\n    def self.create(name, alias_name: nil)\n      alias_name = alias_name.to_s unless alias_name.nil?\n      Attribute.new(name.to_s, alias_name)\n    end\n\n    def ==(other)\n      return name.to_sym == other if other.is_a? Symbol\n      return name == other.name && alias_name == other.alias_name if other.is_a? Panko::Attribute\n\n      super\n    end\n\n    def hash\n      name.to_sym.hash\n    end\n\n    def eql?(other)\n      self.==(other)\n    end\n\n    def inspect\n      \"<Panko::Attribute name=#{name.inspect} alias_name=#{alias_name.inspect}>\"\n    end\n  end\nend\n"
  },
  {
    "path": "lib/panko/object_writer.rb",
    "content": "# frozen_string_literal: true\n\nclass Panko::ObjectWriter\n  def initialize\n    @values = []\n    @keys = []\n\n    @next_key = nil\n    @output = nil\n  end\n\n  def push_object(key = nil)\n    @values << {}\n    @keys << key\n  end\n\n  def push_array(key = nil)\n    @values << []\n    @keys << key\n  end\n\n  def push_key(key)\n    @next_key = key\n  end\n\n  def push_value(value, key = nil)\n    unless @next_key.nil?\n      raise \"push_value is called with key after push_key is called\" unless key.nil?\n      key = @next_key\n      @next_key = nil\n    end\n\n    @values.last[key] = value.as_json\n  end\n\n  def push_json(value, key = nil)\n    if value.is_a?(String)\n      value = begin\n        Oj.load(value)\n      rescue\n        nil\n      end\n    end\n\n    push_value(value, key)\n  end\n\n  def pop\n    result = @values.pop\n\n    if @values.empty?\n      @output = result\n      return\n    end\n\n    scope_key = @keys.pop\n    if scope_key.nil?\n      @values.last << result\n    else\n      @values.last[scope_key] = result\n    end\n  end\n\n  def output\n    raise \"Output is called before poping all\" unless @values.empty?\n    @output\n  end\nend\n"
  },
  {
    "path": "lib/panko/response.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"oj\"\n\nmodule Panko\n  JsonValue = Struct.new(:value) do\n    def self.from(value)\n      JsonValue.new(value)\n    end\n\n    def to_json\n      value\n    end\n  end\n\n  class ResponseCreator\n    def self.value(value)\n      Panko::Response.new(value)\n    end\n\n    def self.json(value)\n      Panko::JsonValue.from(value)\n    end\n\n    def self.array_serializer(data, serializer, options = {})\n      merged_options = options.merge(each_serializer: serializer)\n      Panko::ArraySerializer.new(data, merged_options)\n    end\n\n    def self.serializer(data, serializer, options = {})\n      json serializer.new(options).serialize_to_json(data)\n    end\n  end\n\n  class Response\n    def initialize(data)\n      @data = data\n    end\n\n    def to_json(_options = nil)\n      writer = Oj::StringWriter.new(mode: :rails)\n      write(writer, @data)\n      writer.to_s\n    end\n\n    def self.create\n      Response.new(yield ResponseCreator)\n    end\n\n    private\n\n    def write(writer, data, key = nil)\n      return write_array(writer, data, key) if data.is_a?(Array)\n\n      return write_object(writer, data, key) if data.is_a?(Hash)\n\n      write_value(writer, data, key)\n    end\n\n    def write_array(writer, value, key = nil)\n      writer.push_array key\n      value.each { |v| write(writer, v) }\n      writer.pop\n    end\n\n    def write_object(writer, value, key = nil)\n      writer.push_object key\n\n      value.each do |entry_key, entry_value|\n        write(writer, entry_value, entry_key.to_s)\n      end\n\n      writer.pop\n    end\n\n    def write_value(writer, value, key = nil)\n      if value.is_a?(Panko::ArraySerializer) ||\n          value.is_a?(Panko::Serializer) ||\n          value.is_a?(Panko::Response) ||\n          value.is_a?(Panko::JsonValue)\n        writer.push_json(value.to_json, key)\n      else\n        writer.push_value(value, key)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/panko/serialization_descriptor.rb",
    "content": "# frozen_string_literal: true\n\nmodule Panko\n  class SerializationDescriptor\n    #\n    # Creates new description and apply the options\n    # on the new descriptor\n    #\n    def self.build(serializer, options = {}, serialization_context = nil)\n      backend = Panko::SerializationDescriptor.duplicate(serializer._descriptor)\n\n      options.merge! serializer.filters_for(options[:context], options[:scope]) if serializer.respond_to? :filters_for\n\n      backend.apply_filters(options)\n\n      backend.set_serialization_context(serialization_context)\n\n      backend\n    end\n\n    #\n    # Create new descriptor with same properties\n    # useful when you want to apply filters\n    #\n    def self.duplicate(descriptor)\n      backend = Panko::SerializationDescriptor.new\n\n      backend.type = descriptor.type\n\n      backend.attributes = descriptor.attributes.dup\n\n      backend.method_fields = descriptor.method_fields.dup\n      backend.serializer = descriptor.type.new(_skip_init: true) unless backend.method_fields.empty?\n\n      backend.has_many_associations = descriptor.has_many_associations.map(&:duplicate)\n      backend.has_one_associations = descriptor.has_one_associations.map(&:duplicate)\n\n      backend\n    end\n\n    def set_serialization_context(context)\n      serializer.serialization_context = context if !method_fields.empty? && !serializer.nil?\n\n      has_many_associations.each do |assoc|\n        assoc.descriptor.set_serialization_context context\n      end\n\n      has_one_associations.each do |assoc|\n        assoc.descriptor.set_serialization_context context\n      end\n    end\n\n    #\n    # Applies attributes and association filters\n    #\n    def apply_filters(options)\n      return unless options.key?(:only) || options.key?(:except)\n\n      attributes_only_filters, associations_only_filters = resolve_filters(options, :only)\n      attributes_except_filters, associations_except_filters = resolve_filters(options, :except)\n\n      self.attributes = apply_attribute_filters(\n        attributes,\n        attributes_only_filters,\n        attributes_except_filters\n      )\n\n      self.method_fields = apply_attribute_filters(\n        method_fields,\n        attributes_only_filters,\n        attributes_except_filters\n      )\n\n      unless has_many_associations.empty?\n        self.has_many_associations = apply_association_filters(\n          has_many_associations,\n          {attributes: attributes_only_filters, associations: associations_only_filters},\n          attributes: attributes_except_filters, associations: associations_except_filters\n        )\n      end\n\n      unless has_one_associations.empty?\n        self.has_one_associations = apply_association_filters(\n          has_one_associations,\n          {attributes: attributes_only_filters, associations: associations_only_filters},\n          attributes: attributes_except_filters, associations: associations_except_filters\n        )\n      end\n    end\n\n    def apply_association_filters(associations, only_filters, except_filters)\n      attributes_only_filters = only_filters[:attributes] || []\n      unless attributes_only_filters.empty?\n        associations.select! do |association|\n          attributes_only_filters.include?(association.name_sym)\n        end\n      end\n\n      attributes_except_filters = except_filters[:attributes] || []\n      unless attributes_except_filters.empty?\n        associations.reject! do |association|\n          attributes_except_filters.include?(association.name_sym)\n        end\n      end\n\n      associations_only_filters = only_filters[:associations]\n      associations_except_filters = except_filters[:associations]\n\n      return associations if associations_only_filters.empty? && associations_except_filters.empty?\n\n      associations.map do |association|\n        name = association.name_sym\n        descriptor = association.descriptor\n\n        only_filter = associations_only_filters[name]\n        except_filter = associations_except_filters[name]\n\n        filters = {}\n        filters[:only] = only_filter unless only_filter.nil?\n        filters[:except] = except_filter unless except_filter.nil?\n\n        unless filters.empty?\n          next Panko::Association.new(\n            name,\n            association.name_str,\n            Panko::SerializationDescriptor.build(descriptor.type, filters)\n          )\n        end\n\n        association\n      end\n    end\n\n    def resolve_filters(options, filter)\n      filters = options.fetch(filter, {})\n      return filters, {} if filters.is_a? Array\n\n      # hash filters looks like this\n      # { instance: [:a], foo: [:b] }\n      # which mean, for the current instance use `[:a]` as filter\n      # and for association named `foo` use `[:b]`\n\n      return [], {} if filters.empty?\n\n      attributes_filters = filters.fetch(:instance, [])\n      association_filters = filters.except(:instance)\n\n      [attributes_filters, association_filters]\n    end\n\n    def apply_fields_filters(fields, only, except)\n      return fields & only unless only.empty?\n      return fields - except unless except.empty?\n\n      fields\n    end\n\n    def apply_attribute_filters(attributes, only, except)\n      unless only.empty?\n        attributes = attributes.select do |attribute|\n          name_to_check = attribute.name\n          name_to_check = attribute.alias_name unless attribute.alias_name.nil?\n\n          only.include?(name_to_check.to_sym)\n        end\n      end\n\n      unless except.empty?\n        attributes = attributes.reject do |attribute|\n          name_to_check = attribute.name\n          name_to_check = attribute.alias_name unless attribute.alias_name.nil?\n\n          except.include?(name_to_check.to_sym)\n        end\n      end\n\n      attributes\n    end\n  end\nend\n"
  },
  {
    "path": "lib/panko/serializer.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"serialization_descriptor\"\nrequire \"oj\"\n\nclass SerializationContext\n  attr_accessor :context, :scope\n\n  def initialize(context, scope)\n    @context = context\n    @scope = scope\n  end\n\n  def self.create(options)\n    if options.key?(:context) || options.key?(:scope)\n      SerializationContext.new(options[:context], options[:scope])\n    else\n      EmptySerializerContext.new\n    end\n  end\nend\n\nclass EmptySerializerContext\n  def scope\n    nil\n  end\n\n  def context\n    nil\n  end\nend\n\nmodule Panko\n  class Serializer\n    SKIP = Object.new.freeze\n\n    class << self\n      def inherited(base)\n        if _descriptor.nil?\n          base._descriptor = Panko::SerializationDescriptor.new\n\n          base._descriptor.attributes = []\n          base._descriptor.aliases = {}\n\n          base._descriptor.method_fields = []\n\n          base._descriptor.has_many_associations = []\n          base._descriptor.has_one_associations = []\n        else\n          base._descriptor = Panko::SerializationDescriptor.duplicate(_descriptor)\n        end\n        base._descriptor.type = base\n      end\n\n      attr_accessor :_descriptor\n\n      def attributes(*attrs)\n        @_descriptor.attributes.push(*attrs.map { |attr| Attribute.create(attr) }).uniq!\n      end\n\n      def aliases(aliases = {})\n        aliases.each do |attr, alias_name|\n          @_descriptor.attributes << Attribute.create(attr, alias_name: alias_name)\n        end\n      end\n\n      def method_added(method)\n        super\n\n        return if @_descriptor.nil?\n\n        deleted_attr = @_descriptor.attributes.delete(method)\n        @_descriptor.method_fields << Attribute.create(deleted_attr.name, alias_name: deleted_attr.alias_name) unless deleted_attr.nil?\n      end\n\n      def has_one(name, options = {})\n        serializer_const = options[:serializer]\n        if serializer_const.is_a?(String)\n          serializer_const = Panko::SerializerResolver.resolve(serializer_const, self)\n        end\n        serializer_const ||= Panko::SerializerResolver.resolve(name.to_s, self)\n\n        raise \"Can't find serializer for #{self.name}.#{name} has_one relationship.\" if serializer_const.nil?\n\n        @_descriptor.has_one_associations << Panko::Association.new(\n          name,\n          options.fetch(:name, name).to_s,\n          Panko::SerializationDescriptor.build(serializer_const, options)\n        )\n      end\n\n      def has_many(name, options = {})\n        serializer_const = options[:serializer] || options[:each_serializer]\n        if serializer_const.is_a?(String)\n          serializer_const = Panko::SerializerResolver.resolve(serializer_const, self)\n        end\n        serializer_const ||= Panko::SerializerResolver.resolve(name.to_s, self)\n\n        raise \"Can't find serializer for #{self.name}.#{name} has_many relationship.\" if serializer_const.nil?\n\n        @_descriptor.has_many_associations << Panko::Association.new(\n          name,\n          options.fetch(:name, name).to_s,\n          Panko::SerializationDescriptor.build(serializer_const, options)\n        )\n      end\n    end\n\n    def initialize(options = {})\n      # this \"_skip_init\" trick is so I can create serializers from serialization descriptor\n      return if options[:_skip_init]\n\n      @serialization_context = SerializationContext.create(options)\n      @descriptor = Panko::SerializationDescriptor.build(self.class, options, @serialization_context)\n      @used = false\n    end\n\n    def context\n      @serialization_context.context\n    end\n\n    def scope\n      @serialization_context.scope\n    end\n\n    attr_writer :serialization_context\n    attr_reader :object\n\n    def serialize(object)\n      serialize_with_writer(object, Panko::ObjectWriter.new).output\n    end\n\n    def serialize_to_json(object)\n      serialize_with_writer(object, Oj::StringWriter.new(mode: :rails)).to_s\n    end\n\n    private\n\n    def serialize_with_writer(object, writer)\n      raise ArgumentError.new(\"Panko::Serializer instances are single-use\") if @used\n      Panko.serialize_object(object, writer, @descriptor)\n      @used = true\n      writer\n    end\n  end\nend\n"
  },
  {
    "path": "lib/panko/serializer_resolver.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"active_support/core_ext/string/inflections\"\nrequire \"active_support/core_ext/module/introspection\"\n\nclass Panko::SerializerResolver\n  class << self\n    def resolve(name, from)\n      serializer_const = nil\n\n      namespace = namespace_for(from)\n\n      if namespace.present?\n        serializer_const = safe_serializer_get(\"#{namespace}::#{name.singularize.camelize}Serializer\")\n      end\n\n      serializer_const ||= safe_serializer_get(\"#{name.singularize.camelize}Serializer\")\n      serializer_const ||= safe_serializer_get(name)\n      serializer_const\n    end\n\n    private\n\n    if Module.method_defined?(:module_parent_name)\n      def namespace_for(from)\n        from.module_parent_name\n      end\n    else\n      def namespace_for(from)\n        from.parent_name\n      end\n    end\n\n    def safe_serializer_get(name)\n      const = Object.const_get(name)\n      (const < Panko::Serializer) ? const : nil\n    rescue NameError\n      nil\n    end\n  end\nend\n"
  },
  {
    "path": "lib/panko/version.rb",
    "content": "# frozen_string_literal: true\n\nmodule Panko\n  VERSION = \"0.8.5\"\nend\n"
  },
  {
    "path": "lib/panko_serializer.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"panko/version\"\nrequire \"panko/attribute\"\nrequire \"panko/association\"\nrequire \"panko/serializer\"\nrequire \"panko/array_serializer\"\nrequire \"panko/response\"\nrequire \"panko/serializer_resolver\"\nrequire \"panko/object_writer\"\n\n# C Extension\nrequire \"oj\"\nrequire \"panko/panko_serializer\"\n"
  },
  {
    "path": "panko_serializer.gemspec",
    "content": "# frozen_string_literal: true\n\nlib = File.expand_path(\"lib\", __dir__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\nrequire \"panko/version\"\n\nGem::Specification.new do |spec|\n  spec.name = \"panko_serializer\"\n  spec.version = Panko::VERSION\n  spec.authors = [\"Yosi Attias\"]\n  spec.email = [\"yosy101@gmail.com\"]\n\n  spec.summary = \"High Performance JSON Serialization for ActiveRecord & Ruby Objects\"\n  spec.homepage = \"https://panko.dev\"\n  spec.license = \"MIT\"\n\n  spec.metadata = {\n    \"bug_tracker_uri\" => \"https://github.com/yosiat/panko_serializer/issues\",\n    \"source_code_uri\" => \"https://github.com/yosiat/panko_serializer\",\n    \"documentation_uri\" => \"https://panko.dev\",\n    \"changelog_uri\" => \"https://github.com/yosiat/panko_serializer/releases\"\n  }\n\n  spec.required_ruby_version = \">= 3.1.0\"\n\n  spec.files = Dir[\"{ext,lib}/**/*\", \"LICENSE.txt\", \"README.md\"] & `git ls-files -z`.split(\"\\x0\")\n  spec.require_paths = [\"lib\"]\n\n  spec.extensions << \"ext/panko_serializer/extconf.rb\"\n\n  spec.add_dependency \"oj\", \"> 3.11.0\", \"< 4.0.0\"\n  spec.add_dependency \"activesupport\"\n  spec.add_development_dependency \"appraisal\"\nend\n"
  },
  {
    "path": "spec/features/active_record_serialization_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"ActiveRecord Serialization\" do\n  before do\n    Temping.create(:foo) do\n      with_columns do |t|\n        t.string :name\n        t.string :address\n      end\n    end\n  end\n\n  it \"serializes objects from database\" do\n    class FooSerializer < Panko::Serializer\n      attributes :name, :address\n    end\n\n    foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word).reload\n\n    expect(foo).to serialized_as(FooSerializer,\n      \"name\" => foo.name,\n      \"address\" => foo.address)\n  end\n\n  it \"serializes objects from memory\" do\n    class FooSerializer < Panko::Serializer\n      attributes :name, :address\n    end\n\n    foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n    expect(foo).to serialized_as(FooSerializer,\n      \"name\" => foo.name,\n      \"address\" => foo.address)\n  end\n\n  it \"preserves changed attributes\" do\n    class FooSerializer < Panko::Serializer\n      attributes :name, :address\n    end\n\n    foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word).reload\n\n    foo.update!(name: \"This is a new name\")\n\n    expect(foo).to serialized_as(FooSerializer,\n      \"name\" => \"This is a new name\",\n      \"address\" => foo.address)\n  end\nend\n"
  },
  {
    "path": "spec/features/array_serializer_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe Panko::ArraySerializer do\n  before do\n    Temping.create(:foo) do\n      with_columns do |t|\n        t.string :name\n        t.string :address\n      end\n    end\n  end\n\n  let(:foo_serializer_class) do\n    Class.new(Panko::Serializer) do\n      attributes :name, :address\n    end\n  end\n\n  before { stub_const(\"FooSerializer\", foo_serializer_class) }\n\n  it \"throws argument error when each_serializer isnt passed\" do\n    expect do\n      Panko::ArraySerializer.new([])\n    end.to raise_error(ArgumentError)\n  end\n\n  context \"sanity\" do\n    it \"serializers array of elements\" do\n      array_serializer_factory = -> { Panko::ArraySerializer.new([], each_serializer: FooSerializer) }\n\n      foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(Foo.all).to serialized_as(array_serializer_factory, [\n        {\"name\" => foo1.name, \"address\" => foo1.address},\n        {\"name\" => foo2.name, \"address\" => foo2.address}\n      ])\n    end\n\n    it \"serializes array of elements with virtual attribtues\" do\n      class TestSerializerWithMethodsSerializer < Panko::Serializer\n        attributes :name, :address, :something, :context_fetch\n\n        def something\n          \"#{object.name} #{object.address}\"\n        end\n\n        def context_fetch\n          context[:value]\n        end\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      array_serializer_factory = -> {\n        Panko::ArraySerializer.new([],\n          each_serializer: TestSerializerWithMethodsSerializer,\n          context: {value: 6})\n      }\n\n      expect(Foo.all).to serialized_as(array_serializer_factory, [{\"name\" => foo.name,\n                                                                   \"address\" => foo.address,\n                                                                   \"something\" => \"#{foo.name} #{foo.address}\",\n                                                                   \"context_fetch\" => 6}])\n    end\n  end\n\n  context \"filter\" do\n    it \"only\" do\n      array_serializer_factory = -> { Panko::ArraySerializer.new([], each_serializer: FooSerializer, only: [:name]) }\n\n      foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(Foo.all).to serialized_as(array_serializer_factory, [\n        {\"name\" => foo1.name},\n        {\"name\" => foo2.name}\n      ])\n    end\n\n    it \"except\" do\n      array_serializer_factory = -> { Panko::ArraySerializer.new([], each_serializer: FooSerializer, except: [:name]) }\n\n      foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(Foo.all).to serialized_as(array_serializer_factory, [\n        {\"address\" => foo1.address},\n        {\"address\" => foo2.address}\n      ])\n    end\n  end\nend\n"
  },
  {
    "path": "spec/features/associations_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"Associations Serialization\" do\n  context \"has_one\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n\n      Temping.create(:foo_holder) do\n        with_columns do |t|\n          t.string :name\n          t.references :foo\n        end\n\n        belongs_to :foo, optional: true\n      end\n    end\n\n    let(:foo_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name, :address\n      end\n    end\n\n    before { stub_const(\"FooSerializer\", foo_serializer_class) }\n\n    it \"serializes plain object associations\" do\n      class PlainFooHolder\n        attr_accessor :name, :foo\n\n        def initialize(name, foo)\n          @name = name\n          @foo = foo\n        end\n      end\n\n      class PlainFoo\n        attr_accessor :name, :address\n\n        def initialize(name, address)\n          @name = name\n          @address = address\n        end\n      end\n\n      class FooSerializer < Panko::Serializer\n        attributes :name, :address\n      end\n\n      class PlainFooHolderHasOneSerializer < Panko::Serializer\n        attributes :name\n\n        has_one :foo, serializer: FooSerializer\n      end\n\n      foo = PlainFoo.new(Faker::Lorem.word, Faker::Lorem.word)\n      foo_holder = PlainFooHolder.new(Faker::Lorem.word, foo)\n\n      expect(foo_holder).to serialized_as(PlainFooHolderHasOneSerializer, \"name\" => foo_holder.name,\n        \"foo\" => {\n          \"name\" => foo.name,\n          \"address\" => foo.address\n        })\n    end\n\n    it \"accepts serializer name as string\" do\n      class FooHolderHasOneWithStringSerializer < Panko::Serializer\n        attributes :name\n\n        has_one :foo, serializer: \"FooSerializer\"\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)\n\n      expect(foo_holder).to serialized_as(FooHolderHasOneWithStringSerializer, \"name\" => foo_holder.name,\n        \"foo\" => {\n          \"name\" => foo.name,\n          \"address\" => foo.address\n        })\n    end\n\n    it \"accepts name option\" do\n      class FooHolderHasOneWithNameSerializer < Panko::Serializer\n        attributes :name\n\n        has_one :foo, serializer: FooSerializer, name: :my_foo\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)\n\n      expect(foo_holder).to serialized_as(FooHolderHasOneWithNameSerializer, \"name\" => foo_holder.name,\n        \"my_foo\" => {\n          \"name\" => foo.name,\n          \"address\" => foo.address\n        })\n    end\n\n    it \"serializes using the :serializer option\" do\n      class FooHolderHasOneSerializer < Panko::Serializer\n        attributes :name\n\n        has_one :foo, serializer: FooSerializer\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)\n\n      expect(foo_holder).to serialized_as(FooHolderHasOneSerializer, \"name\" => foo_holder.name,\n        \"foo\" => {\n          \"name\" => foo.name,\n          \"address\" => foo.address\n        })\n    end\n\n    it \"infers the serializer name by name of the relationship\" do\n      class FooHolderHasOneSerializer < Panko::Serializer\n        attributes :name\n\n        has_one :foo\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)\n\n      expect(foo_holder).to serialized_as(FooHolderHasOneSerializer, \"name\" => foo_holder.name,\n        \"foo\" => {\n          \"name\" => foo.name,\n          \"address\" => foo.address\n        })\n    end\n\n    it \"raises if it can't find the serializer\" do\n      expect do\n        class NotFoundHasOneSerializer < Panko::Serializer\n          attributes :name\n\n          has_one :not_existing_serializer\n        end\n      end.to raise_error(\"Can't find serializer for NotFoundHasOneSerializer.not_existing_serializer has_one relationship.\")\n    end\n\n    it \"allows virtual method in has one serializer\" do\n      class VirtualSerializer < Panko::Serializer\n        attributes :virtual\n\n        def virtual\n          \"Hello #{object.name}\"\n        end\n      end\n\n      class FooHolderHasOneVirtualSerializer < Panko::Serializer\n        attributes :name\n\n        has_one :foo, serializer: VirtualSerializer\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)\n\n      expect(foo_holder).to serialized_as(FooHolderHasOneVirtualSerializer, \"name\" => foo_holder.name,\n        \"foo\" => {\n          \"virtual\" => \"Hello #{foo.name}\"\n        })\n    end\n\n    it \"handles nil\" do\n      class FooHolderHasOneSerializer < Panko::Serializer\n        attributes :name\n\n        has_one :foo, serializer: FooSerializer\n      end\n\n      foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: nil)\n\n      expect(foo_holder).to serialized_as(FooHolderHasOneSerializer, \"name\" => foo_holder.name,\n        \"foo\" => nil)\n    end\n  end\n\n  context \"has_one with different model types\" do\n    it \"can use the serializer string name when resolving the serializer\" do\n      Temping.create(:goo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n\n      Temping.create(:foo_holder) do\n        with_columns do |t|\n          t.string :name\n          t.references :goo\n        end\n\n        belongs_to :goo\n      end\n\n      class FooSerializer < Panko::Serializer\n        attributes :name, :address\n      end\n\n      class FooHolderHasOnePooWithStringSerializer < Panko::Serializer\n        attributes :name\n\n        has_one :goo, serializer: \"FooSerializer\"\n      end\n\n      goo = Goo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo_holder = FooHolder.create(name: Faker::Lorem.word, goo: goo)\n\n      expect(foo_holder).to serialized_as(\n        FooHolderHasOnePooWithStringSerializer,\n        \"name\" => foo_holder.name,\n        \"goo\" => {\n          \"name\" => goo.name,\n          \"address\" => goo.address\n        }\n      )\n    end\n  end\n\n  context \"has_many\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n          t.references :foos_holder\n        end\n\n        belongs_to :foos_holder, optional: true\n      end\n\n      Temping.create(:foos_holder) do\n        with_columns do |t|\n          t.string :name\n        end\n\n        has_many :foos\n      end\n    end\n\n    let(:foo_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name, :address\n      end\n    end\n\n    before { stub_const(\"FooSerializer\", foo_serializer_class) }\n\n    it \"serializes using the :serializer option\" do\n      class FoosHasManyHolderSerializer < Panko::Serializer\n        attributes :name\n\n        has_many :foos, serializer: FooSerializer\n      end\n\n      foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])\n\n      expect(foos_holder).to serialized_as(FoosHasManyHolderSerializer, \"name\" => foos_holder.name,\n        \"foos\" => [\n          {\n            \"name\" => foo1.name,\n            \"address\" => foo1.address\n          },\n          {\n            \"name\" => foo2.name,\n            \"address\" => foo2.address\n          }\n        ])\n    end\n\n    it \"accepts serializer name as string\" do\n      class FoosHasManyHolderSerializer < Panko::Serializer\n        attributes :name\n\n        has_many :foos, serializer: \"FooSerializer\"\n      end\n\n      foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])\n\n      expect(foos_holder).to serialized_as(FoosHasManyHolderSerializer, \"name\" => foos_holder.name,\n        \"foos\" => [\n          {\n            \"name\" => foo1.name,\n            \"address\" => foo1.address\n          },\n          {\n            \"name\" => foo2.name,\n            \"address\" => foo2.address\n          }\n        ])\n    end\n\n    it \"supports :name\" do\n      class FoosHasManyHolderWithNameSerializer < Panko::Serializer\n        attributes :name\n\n        has_many :foos, serializer: FooSerializer, name: :my_foos\n      end\n\n      foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])\n\n      expect(foos_holder).to serialized_as(FoosHasManyHolderWithNameSerializer, \"name\" => foos_holder.name,\n        \"my_foos\" => [\n          {\n            \"name\" => foo1.name,\n            \"address\" => foo1.address\n          },\n          {\n            \"name\" => foo2.name,\n            \"address\" => foo2.address\n          }\n        ])\n    end\n\n    it \"infers the serializer name by name of the relationship\" do\n      class FoosHasManyHolderSerializer < Panko::Serializer\n        attributes :name\n\n        has_many :foos\n      end\n\n      foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])\n\n      expect(foos_holder).to serialized_as(FoosHasManyHolderSerializer, \"name\" => foos_holder.name,\n        \"foos\" => [\n          {\n            \"name\" => foo1.name,\n            \"address\" => foo1.address\n          },\n          {\n            \"name\" => foo2.name,\n            \"address\" => foo2.address\n          }\n        ])\n    end\n\n    it \"raises if it can't find the serializer\" do\n      expect do\n        class NotFoundHasManySerializer < Panko::Serializer\n          attributes :name\n\n          has_many :not_existing_serializers\n        end\n      end.to raise_error(\"Can't find serializer for NotFoundHasManySerializer.not_existing_serializers has_many relationship.\")\n    end\n\n    it \"serializes using the :each_serializer option\" do\n      class FoosHasManyHolderSerializer < Panko::Serializer\n        attributes :name\n\n        has_many :foos, each_serializer: FooSerializer\n      end\n\n      foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])\n\n      expect(foos_holder).to serialized_as(FoosHasManyHolderSerializer, \"name\" => foos_holder.name,\n        \"foos\" => [\n          {\n            \"name\" => foo1.name,\n            \"address\" => foo1.address\n          },\n          {\n            \"name\" => foo2.name,\n            \"address\" => foo2.address\n          }\n        ])\n    end\n\n    it \"accepts only as option\" do\n      class FoosHolderWithOnlySerializer < Panko::Serializer\n        attributes :name\n\n        has_many :foos, serializer: FooSerializer, only: [:address]\n      end\n\n      foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])\n\n      expect(foos_holder).to serialized_as(FoosHolderWithOnlySerializer, \"name\" => foos_holder.name,\n        \"foos\" => [\n          {\n            \"address\" => foo1.address\n          },\n          {\n            \"address\" => foo2.address\n          }\n        ])\n    end\n  end\n\n  context \"has_many with different model types\" do\n    it \"uses the serializer string name when resolving the serializer\" do\n      Temping.create(:goo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n          t.references :foos_holder\n        end\n\n        belongs_to :foos_holder, optional: true\n      end\n\n      Temping.create(:foos_holder) do\n        with_columns do |t|\n          t.string :name\n        end\n\n        has_many :goos\n      end\n\n      class FooSerializer < Panko::Serializer\n        attributes :name, :address\n      end\n\n      class FoosHasManyPoosHolderSerializer < Panko::Serializer\n        attributes :name\n\n        has_many :goos, serializer: \"FooSerializer\"\n      end\n\n      goo1 = Goo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      goo2 = Goo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foos_holder = FoosHolder.create(name: Faker::Lorem.word, goos: [goo1, goo2])\n\n      expect(foos_holder).to serialized_as(\n        FoosHasManyPoosHolderSerializer,\n        \"name\" => foos_holder.name,\n        \"goos\" => [\n          {\n            \"name\" => goo1.name,\n            \"address\" => goo1.address\n          },\n          {\n            \"name\" => goo2.name,\n            \"address\" => goo2.address\n          }\n        ]\n      )\n    end\n  end\n\n  context \"polymorphic associations\" do\n    it \"serializes polymorphic has_many associations\" do\n      Temping.create(:comment) do\n        with_columns do |t|\n          t.string :content\n          t.references :commentable, polymorphic: true\n        end\n\n        belongs_to :commentable, polymorphic: true\n      end\n\n      Temping.create(:post) do\n        with_columns do |t|\n          t.string :title\n          t.string :content\n        end\n\n        has_many :comments, as: :commentable\n      end\n\n      class CommentSerializer < Panko::Serializer\n        attributes :content\n      end\n\n      class PostSerializer < Panko::Serializer\n        attributes :title, :content\n        has_many :comments, serializer: CommentSerializer\n      end\n\n      post = Post.create(title: Faker::Lorem.word, content: Faker::Lorem.sentence)\n      comment1 = Comment.create(content: Faker::Lorem.sentence, commentable: post)\n      comment2 = Comment.create(content: Faker::Lorem.sentence, commentable: post)\n\n      expect(post).to serialized_as(PostSerializer,\n        \"title\" => post.title,\n        \"content\" => post.content,\n        \"comments\" => [\n          {\"content\" => comment1.content},\n          {\"content\" => comment2.content}\n        ])\n    end\n\n    it \"serializes polymorphic associations with different models\" do\n      Temping.create(:comment) do\n        with_columns do |t|\n          t.string :content\n          t.references :commentable, polymorphic: true\n        end\n\n        belongs_to :commentable, polymorphic: true\n      end\n\n      Temping.create(:article) do\n        with_columns do |t|\n          t.string :title\n          t.string :body\n        end\n\n        has_many :comments, as: :commentable\n      end\n\n      class CommentSerializer < Panko::Serializer\n        attributes :content\n      end\n\n      class ArticleSerializer < Panko::Serializer\n        attributes :title, :body\n        has_many :comments, serializer: CommentSerializer\n      end\n\n      article = Article.create(title: Faker::Lorem.word, body: Faker::Lorem.sentence)\n      comment = Comment.create(content: Faker::Lorem.sentence, commentable: article)\n\n      expect(article).to serialized_as(ArticleSerializer,\n        \"title\" => article.title,\n        \"body\" => article.body,\n        \"comments\" => [\n          {\"content\" => comment.content}\n        ])\n    end\n  end\n\n  context \"deeply nested associations\" do\n    it \"serializes 2+ levels of nesting\" do\n      Temping.create(:user) do\n        with_columns do |t|\n          t.string :name\n          t.string :email\n          t.references :team\n        end\n\n        belongs_to :team\n      end\n\n      Temping.create(:team) do\n        with_columns do |t|\n          t.string :name\n          t.references :organization\n        end\n\n        belongs_to :organization\n        has_many :users\n      end\n\n      Temping.create(:organization) do\n        with_columns do |t|\n          t.string :name\n        end\n\n        has_many :teams\n        has_many :users, through: :teams\n      end\n\n      class UserSerializer < Panko::Serializer\n        attributes :name, :email\n      end\n\n      class TeamSerializer < Panko::Serializer\n        attributes :name\n        has_many :users, serializer: UserSerializer\n      end\n\n      class OrganizationSerializer < Panko::Serializer\n        attributes :name\n        has_many :teams, serializer: TeamSerializer\n      end\n\n      org = Organization.create(name: Faker::Company.name)\n      team1 = Team.create(name: Faker::Team.name, organization: org)\n      team2 = Team.create(name: Faker::Team.name, organization: org)\n      user1 = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team1)\n      user2 = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team2)\n\n      expect(org).to serialized_as(OrganizationSerializer,\n        \"name\" => org.name,\n        \"teams\" => [\n          {\n            \"name\" => team1.name,\n            \"users\" => [\n              {\n                \"name\" => user1.name,\n                \"email\" => user1.email\n              }\n            ]\n          },\n          {\n            \"name\" => team2.name,\n            \"users\" => [\n              {\n                \"name\" => user2.name,\n                \"email\" => user2.email\n              }\n            ]\n          }\n        ])\n    end\n  end\n\n  context \"combined options\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n          t.references :foos_holder\n        end\n\n        belongs_to :foos_holder, optional: true\n      end\n\n      Temping.create(:foos_holder) do\n        with_columns do |t|\n          t.string :name\n        end\n\n        has_many :foos\n      end\n\n      Temping.create(:foo_holder) do\n        with_columns do |t|\n          t.string :name\n          t.references :foo\n        end\n\n        belongs_to :foo\n      end\n    end\n\n    let(:foo_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name, :address\n      end\n    end\n\n    before { stub_const(\"FooSerializer\", foo_serializer_class) }\n\n    it \"handles multiple options together (name, serializer, only)\" do\n      class FoosHolderCombinedOptionsSerializer < Panko::Serializer\n        attributes :name\n\n        has_many :foos, serializer: FooSerializer, name: :my_items, only: [:name]\n      end\n\n      foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])\n\n      expect(foos_holder).to serialized_as(FoosHolderCombinedOptionsSerializer,\n        \"name\" => foos_holder.name,\n        \"my_items\" => [\n          {\"name\" => foo1.name},\n          {\"name\" => foo2.name}\n        ])\n    end\n\n    it \"handles has_one with multiple options\" do\n      class FooHolderCombinedOptionsSerializer < Panko::Serializer\n        attributes :name\n\n        has_one :foo, serializer: FooSerializer, name: :my_foo\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)\n\n      expect(foo_holder).to serialized_as(FooHolderCombinedOptionsSerializer,\n        \"name\" => foo_holder.name,\n        \"my_foo\" => {\n          \"name\" => foo.name,\n          \"address\" => foo.address\n        })\n    end\n  end\n\n  context \"nil and empty associations\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n          t.references :foos_holder\n        end\n\n        belongs_to :foos_holder, optional: true\n      end\n\n      Temping.create(:foo_holder) do\n        with_columns do |t|\n          t.string :name\n          t.references :foo\n        end\n\n        belongs_to :foo, optional: true\n      end\n\n      Temping.create(:foos_holder) do\n        with_columns do |t|\n          t.string :name\n        end\n\n        has_many :foos\n      end\n    end\n\n    let(:foo_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name, :address\n      end\n    end\n\n    before { stub_const(\"FooSerializer\", foo_serializer_class) }\n\n    it \"explicitly handles has_one returning nil\" do\n      class FooHolderNilSerializer < Panko::Serializer\n        attributes :name\n\n        has_one :foo, serializer: FooSerializer\n      end\n\n      foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: nil)\n\n      expect(foo_holder).to serialized_as(FooHolderNilSerializer,\n        \"name\" => foo_holder.name,\n        \"foo\" => nil)\n    end\n\n    it \"explicitly handles has_many returning empty array\" do\n      class FoosHolderEmptySerializer < Panko::Serializer\n        attributes :name\n\n        has_many :foos, serializer: FooSerializer\n      end\n\n      foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [])\n\n      expect(foos_holder).to serialized_as(FoosHolderEmptySerializer,\n        \"name\" => foos_holder.name,\n        \"foos\" => [])\n    end\n  end\n\n  context \"invalid associations\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n\n      Temping.create(:foo_holder) do\n        with_columns do |t|\n          t.string :name\n          t.references :foo\n        end\n\n        belongs_to :foo\n      end\n    end\n\n    it \"handles when associated object is not of expected type\" do\n      # This test verifies graceful handling when an association returns an unexpected type\n      class FlexibleSerializer < Panko::Serializer\n        attributes :name, :address\n\n        def name\n          # Handle case where object might not respond to name\n          object.respond_to?(:name) ? object.name : \"unknown\"\n        end\n\n        def address\n          object.respond_to?(:address) ? object.address : \"unknown\"\n        end\n      end\n\n      class FooHolderFlexibleSerializer < Panko::Serializer\n        attributes :name\n\n        has_one :foo, serializer: FlexibleSerializer\n      end\n\n      # Create a foo_holder with a regular foo\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)\n\n      expect(foo_holder).to serialized_as(FooHolderFlexibleSerializer,\n        \"name\" => foo_holder.name,\n        \"foo\" => {\n          \"name\" => foo.name,\n          \"address\" => foo.address\n        })\n    end\n  end\nend\n"
  },
  {
    "path": "spec/features/attributes_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"Attributes Serialization\" do\n  context \"instance variables\" do\n    it \"serializes instance variables\" do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n\n      class FooSerializer < Panko::Serializer\n        attributes :name, :address\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(foo).to serialized_as(FooSerializer,\n        \"name\" => foo.name,\n        \"address\" => foo.address)\n    end\n  end\n\n  context \"method attributes\" do\n    it \"serializes method attributes\" do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n\n      class FooWithMethodsSerializer < Panko::Serializer\n        attributes :name, :address, :something\n\n        def something\n          \"#{object.name} #{object.address}\"\n        end\n\n        def another_method\n          raise \"I shouldn't get called\"\n        end\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(foo).to serialized_as(FooWithMethodsSerializer, \"name\" => foo.name,\n        \"address\" => foo.address,\n        \"something\" => \"#{foo.name} #{foo.address}\")\n    end\n  end\n\n  context \"inheritance\" do\n    it \"supports serializer inheritance\" do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n\n      class BaseSerializer < Panko::Serializer\n        attributes :name\n      end\n\n      class ChildSerializer < BaseSerializer\n        attributes :address\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(foo).to serialized_as(ChildSerializer, \"name\" => foo.name,\n        \"address\" => foo.address)\n    end\n  end\n\n  context \"time serialization\" do\n    it \"serializes time correctly\" do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n          t.timestamps\n        end\n      end\n\n      class ObjectWithTimeSerializer < Panko::Serializer\n        attributes :created_at, :method\n\n        def method\n          object.created_at\n        end\n      end\n\n      obj = Foo.create\n\n      expect(obj).to serialized_as(ObjectWithTimeSerializer,\n        \"created_at\" => obj.created_at.as_json,\n        \"method\" => obj.created_at.as_json)\n    end\n  end\n\n  context \"type handling\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :value\n        end\n      end\n    end\n\n    it \"honors additional types\" do\n      class FooValueSerializer < Panko::Serializer\n        attributes :value\n      end\n\n      foo = Foo.instantiate({\"value\" => \"1\"},\n        \"value\" => ActiveRecord::Type::Integer.new)\n\n      expect(foo).to serialized_as(FooValueSerializer, \"value\" => 1)\n    end\n  end\n\n  context \"aliases\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n    end\n\n    it \"supports active record alias attributes\" do\n      class FooWithAliasesModel < ActiveRecord::Base\n        self.table_name = \"foos\"\n        alias_attribute :full_name, :name\n      end\n\n      class FooWithArAliasesSerializer < Panko::Serializer\n        attributes :full_name, :address\n      end\n\n      foo = FooWithAliasesModel.create(full_name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(foo).to serialized_as(FooWithArAliasesSerializer, \"full_name\" => foo.name, \"address\" => foo.address)\n    end\n\n    it \"allows to alias attributes\" do\n      class FooWithAliasesSerializer < Panko::Serializer\n        attributes :address\n\n        aliases name: :full_name\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(foo).to serialized_as(FooWithAliasesSerializer, \"full_name\" => foo.name, \"address\" => foo.address)\n    end\n\n    context \"alias with method_fields\" do\n      let(:data) { {\"created_at\" => created_at} }\n      let(:created_at) { \"2023-04-18T09:24:41+00:00\" }\n\n      context \"with alias\" do\n        let(:serializer_class) do\n          Class.new(Panko::Serializer) do\n            aliases({created_at: :createdAt})\n          end\n        end\n\n        it \"has createdAt\" do\n          expect(data).to serialized_as(serializer_class,\n            \"createdAt\" => created_at)\n        end\n      end\n\n      context \"with alias + method_fields\" do\n        let(:serializer_class) do\n          Class.new(Panko::Serializer) do\n            aliases({created_at: :createdAt})\n\n            def created_at\n              \"2023-04-18T09:24:41+00:00\"\n            end\n          end\n        end\n\n        it \"has createdAt\" do\n          expect(data).to serialized_as(serializer_class,\n            \"createdAt\" => created_at)\n        end\n      end\n    end\n  end\n\n  context \"null values\" do\n    it \"serializes null values\" do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n\n      class FooSerializer < Panko::Serializer\n        attributes :name, :address\n      end\n\n      expect(Foo.create).to serialized_as(FooSerializer, \"name\" => nil, \"address\" => nil)\n    end\n  end\n\n  context \"SKIP functionality\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n    end\n\n    it \"can skip fields\" do\n      class FooSkipSerializer < FooSerializer\n        def address\n          object.address || SKIP\n        end\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      expect(foo).to serialized_as(FooSkipSerializer, \"name\" => foo.name, \"address\" => foo.address)\n\n      foo = Foo.create(name: Faker::Lorem.word, address: nil)\n      expect(foo).to serialized_as(FooSkipSerializer, \"name\" => foo.name)\n    end\n  end\n\n  context \"serializer reuse\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n    end\n\n    it \"raises an error when reusing serializer instances\" do\n      class FooSerializer < Panko::Serializer\n        attributes :name, :address\n      end\n\n      serializer = FooSerializer.new\n      foo_a = Foo.create\n      foo_b = Foo.create\n\n      expect { serializer.serialize(foo_a) }.not_to raise_error\n      expect { serializer.serialize(foo_b) }.to raise_error(ArgumentError, \"Panko::Serializer instances are single-use\")\n    end\n  end\nend\n"
  },
  {
    "path": "spec/features/context_and_scope_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"Context and Scope\" do\n  context \"context\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n    end\n\n    it \"passes context to attribute methods\" do\n      class FooWithContextSerializer < Panko::Serializer\n        attributes :name, :context_value\n\n        def context_value\n          context[:value]\n        end\n      end\n\n      context = {value: Faker::Lorem.word}\n      serializer_factory = -> { FooWithContextSerializer.new(context: context) }\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(foo).to serialized_as(serializer_factory,\n        \"name\" => foo.name,\n        \"context_value\" => context[:value])\n    end\n  end\n\n  context \"scope\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n\n      Temping.create(:foo_holder) do\n        with_columns do |t|\n          t.string :name\n          t.references :foo\n        end\n\n        belongs_to :foo\n      end\n    end\n\n    let(:foo_with_scope_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :scope_value\n\n        def scope_value\n          scope\n        end\n      end\n    end\n\n    let(:foo_holder_with_scope_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :scope_value\n\n        has_one :foo, serializer: \"FooWithScopeSerializer\"\n\n        def scope_value\n          scope\n        end\n      end\n    end\n\n    before do\n      stub_const(\"FooWithScopeSerializer\", foo_with_scope_serializer_class)\n      stub_const(\"FooHolderWithScopeSerializer\", foo_holder_with_scope_serializer_class)\n    end\n\n    it \"passes scope to attribute methods\" do\n      scope = 123\n      serializer_factory = -> { FooHolderWithScopeSerializer.new(scope: scope) }\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)\n\n      expect(foo_holder).to serialized_as(serializer_factory,\n        \"scope_value\" => scope,\n        \"foo\" => {\n          \"scope_value\" => scope\n        })\n    end\n\n    it \"default scope is nil\" do\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo_holder = FooHolder.create(name: Faker::Lorem.word, foo: foo)\n\n      expect(foo_holder).to serialized_as(FooHolderWithScopeSerializer, \"scope_value\" => nil,\n        \"foo\" => {\n          \"scope_value\" => nil\n        })\n    end\n  end\nend\n"
  },
  {
    "path": "spec/features/filtering_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"Filtering Serialization\" do\n  context \"basic filtering\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n    end\n\n    let(:foo_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name, :address\n      end\n    end\n\n    before { stub_const(\"FooSerializer\", foo_serializer_class) }\n\n    it \"supports only filter\" do\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(foo).to serialized_as(-> { FooSerializer.new(only: [:name]) }, \"name\" => foo.name)\n    end\n\n    it \"supports except filter\" do\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(foo).to serialized_as(-> { FooSerializer.new(except: [:name]) }, \"address\" => foo.address)\n    end\n  end\n\n  context \"association filtering\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n          t.references :foos_holder\n        end\n\n        belongs_to :foos_holder, optional: true\n      end\n\n      Temping.create(:foos_holder) do\n        with_columns do |t|\n          t.string :name\n        end\n\n        has_many :foos\n      end\n    end\n\n    let(:foo_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name, :address\n      end\n    end\n\n    before { stub_const(\"FooSerializer\", foo_serializer_class) }\n\n    it \"filters associations\" do\n      class FoosHolderForFilterTestSerializer < Panko::Serializer\n        attributes :name\n\n        has_many :foos, serializer: FooSerializer\n      end\n\n      serializer_factory = -> { FoosHolderForFilterTestSerializer.new(only: [:foos]) }\n\n      foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])\n\n      expect(foos_holder).to serialized_as(serializer_factory, \"foos\" => [\n        {\n          \"name\" => foo1.name,\n          \"address\" => foo1.address\n        },\n        {\n          \"name\" => foo2.name,\n          \"address\" => foo2.address\n        }\n      ])\n    end\n\n    it \"filters association attributes\" do\n      class FoosHolderForFilterTestSerializer < Panko::Serializer\n        attributes :name\n\n        has_many :foos, serializer: FooSerializer\n      end\n\n      serializer_factory = -> { FoosHolderForFilterTestSerializer.new(only: {foos: [:name]}) }\n\n      foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      foos_holder = FoosHolder.create(name: Faker::Lorem.word, foos: [foo1, foo2])\n\n      expect(foos_holder).to serialized_as(serializer_factory, \"name\" => foos_holder.name,\n        \"foos\" => [\n          {\n            \"name\" => foo1.name\n          },\n          {\n            \"name\" => foo2.name\n          }\n        ])\n    end\n  end\n\n  context \"complex nested filters\" do\n    before do\n      Temping.create(:user) do\n        with_columns do |t|\n          t.string :name\n          t.string :email\n          t.references :team\n        end\n\n        belongs_to :team\n      end\n\n      Temping.create(:team) do\n        with_columns do |t|\n          t.string :name\n          t.references :organization\n        end\n\n        belongs_to :organization\n        has_many :users\n      end\n\n      Temping.create(:organization) do\n        with_columns do |t|\n          t.string :name\n        end\n\n        has_many :teams\n        has_many :users, through: :teams\n      end\n    end\n\n    let(:user_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name, :email\n      end\n    end\n\n    let(:team_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name\n        has_many :users, serializer: \"UserSerializer\"\n      end\n    end\n\n    let(:organization_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name\n        has_many :teams, serializer: \"TeamSerializer\"\n      end\n    end\n\n    before do\n      stub_const(\"UserSerializer\", user_serializer_class)\n      stub_const(\"TeamSerializer\", team_serializer_class)\n      stub_const(\"OrganizationSerializer\", organization_serializer_class)\n    end\n\n    it \"supports complex nested only clauses on has_many\" do\n      org = Organization.create(name: Faker::Company.name)\n      team = Team.create(name: Faker::Team.name, organization: org)\n      user = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team)\n\n      serializer_factory = -> { OrganizationSerializer.new(only: {teams: {users: [:email]}}) }\n\n      expect(org).to serialized_as(serializer_factory,\n        \"name\" => org.name,\n        \"teams\" => [\n          {\n            \"name\" => team.name,\n            \"users\" => [\n              {\"email\" => user.email}\n            ]\n          }\n        ])\n    end\n\n    it \"supports complex nested only clauses on has_one\" do\n      class TeamWithOrgSerializer < Panko::Serializer\n        attributes :name\n        has_one :organization, serializer: OrganizationSerializer\n      end\n\n      org = Organization.create(name: Faker::Company.name)\n      team = Team.create(name: Faker::Team.name, organization: org)\n\n      serializer_factory = -> { TeamWithOrgSerializer.new(only: {organization: [:name]}) }\n\n      expect(team).to serialized_as(serializer_factory,\n        \"name\" => team.name,\n        \"organization\" => {\n          \"name\" => org.name\n        })\n    end\n  end\n\n  context \"aliased attribute filtering\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n    end\n\n    it \"filters based on aliased attribute name\" do\n      class FooWithAliasFilterSerializer < Panko::Serializer\n        attributes :address\n\n        aliases name: :full_name\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      # Filter using the alias name\n      expect(foo).to serialized_as(-> { FooWithAliasFilterSerializer.new(only: [:full_name]) },\n        \"full_name\" => foo.name)\n\n      # Filter using except should also work\n      expect(foo).to serialized_as(-> { FooWithAliasFilterSerializer.new(except: [:address]) },\n        \"full_name\" => foo.name)\n    end\n  end\n\n  context \"filter interactions\" do\n    before do\n      Temping.create(:user) do\n        with_columns do |t|\n          t.string :name\n          t.string :email\n          t.references :team\n        end\n\n        belongs_to :team\n      end\n\n      Temping.create(:team) do\n        with_columns do |t|\n          t.string :name\n        end\n\n        has_many :users\n      end\n    end\n\n    let(:user_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name, :email\n      end\n    end\n\n    before { stub_const(\"UserSerializer\", user_serializer_class) }\n\n    it \"tests interaction between ArraySerializer filters and has_many association filters\" do\n      class TeamArrayFilterSerializer < Panko::Serializer\n        attributes :name\n        has_many :users, serializer: UserSerializer\n      end\n\n      team = Team.create(name: Faker::Team.name)\n      user1 = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team)\n      user2 = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team)\n\n      # ArraySerializer filter should work with association filters\n      array_serializer = Panko::ArraySerializer.new([team], each_serializer: TeamArrayFilterSerializer, only: {users: [:name]})\n      result = array_serializer.to_json\n\n      expected = [\n        {\n          \"name\" => team.name,\n          \"users\" => [\n            {\"name\" => user1.name},\n            {\"name\" => user2.name}\n          ]\n        }\n      ]\n\n      expect(JSON.parse(result)).to eq(expected)\n    end\n  end\n\n  context \"nested except filters\" do\n    before do\n      Temping.create(:user) do\n        with_columns do |t|\n          t.string :name\n          t.string :email\n          t.references :team\n        end\n\n        belongs_to :team\n      end\n\n      Temping.create(:team) do\n        with_columns do |t|\n          t.string :name\n        end\n\n        has_many :users\n      end\n    end\n\n    let(:user_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name, :email\n      end\n    end\n\n    before { stub_const(\"UserSerializer\", user_serializer_class) }\n\n    it \"supports nested except filters\" do\n      class TeamWithExceptSerializer < Panko::Serializer\n        attributes :name\n        has_many :users, serializer: UserSerializer\n      end\n\n      team = Team.create(name: Faker::Team.name)\n      user = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team)\n\n      serializer_factory = -> { TeamWithExceptSerializer.new(except: {users: [:email]}) }\n\n      expect(team).to serialized_as(serializer_factory,\n        \"name\" => team.name,\n        \"users\" => [\n          {\"name\" => user.name}\n        ])\n    end\n  end\n\n  context \"only vs except precedence\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n    end\n\n    let(:foo_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name, :address\n      end\n    end\n\n    before { stub_const(\"FooSerializer\", foo_serializer_class) }\n\n    it \"only takes precedence over except when both are provided\" do\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(foo).to serialized_as(-> { FooSerializer.new(only: [:name], except: [:address]) }, \"name\" => foo.name)\n    end\n  end\n\n  context \"filters_for interaction\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n    end\n\n    it \"combines filters_for with constructor only/except options\" do\n      class FooWithFiltersForSerializer < Panko::Serializer\n        attributes :name, :address\n\n        def self.filters_for(context, scope)\n          {except: [:address]}\n        end\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      # filters_for except should be combined with constructor only\n      expect(foo).to serialized_as(-> { FooWithFiltersForSerializer.new(only: [:name]) }, \"name\" => foo.name)\n    end\n\n    it \"passes context and scope to filters_for method\" do\n      class FooWithContextFiltersSerializer < Panko::Serializer\n        attributes :name, :address\n\n        def self.filters_for(context, scope)\n          if context[:user_role] == \"admin\"\n            {only: [:name, :address]}\n          else\n            {only: [:name]}\n          end\n        end\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      # Test with admin context\n      expect(foo).to serialized_as(-> { FooWithContextFiltersSerializer.new(context: {user_role: \"admin\"}) },\n        \"name\" => foo.name, \"address\" => foo.address)\n\n      # Test with regular user context\n      expect(foo).to serialized_as(-> { FooWithContextFiltersSerializer.new(context: {user_role: \"user\"}) },\n        \"name\" => foo.name)\n    end\n  end\n\n  context \"deeply nested filters\" do\n    before do\n      Temping.create(:user) do\n        with_columns do |t|\n          t.string :name\n          t.string :email\n          t.references :team\n        end\n\n        belongs_to :team\n      end\n\n      Temping.create(:team) do\n        with_columns do |t|\n          t.string :name\n          t.references :organization\n        end\n\n        belongs_to :organization\n        has_many :users\n      end\n\n      Temping.create(:organization) do\n        with_columns do |t|\n          t.string :name\n        end\n\n        has_many :teams\n        has_many :users, through: :teams\n      end\n    end\n\n    let(:user_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name, :email\n      end\n    end\n\n    let(:team_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name\n        has_many :users, serializer: \"UserSerializer\"\n      end\n    end\n\n    let(:organization_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name\n        has_many :teams, serializer: \"TeamSerializer\"\n      end\n    end\n\n    before do\n      stub_const(\"UserSerializer\", user_serializer_class)\n      stub_const(\"TeamSerializer\", team_serializer_class)\n      stub_const(\"OrganizationSerializer\", organization_serializer_class)\n    end\n\n    it \"filters on 2+ levels deep associations\" do\n      org = Organization.create(name: Faker::Company.name)\n      team = Team.create(name: Faker::Team.name, organization: org)\n      user = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team)\n\n      serializer_factory = -> { OrganizationSerializer.new(only: {teams: {users: [:name]}}) }\n\n      expect(org).to serialized_as(serializer_factory,\n        \"name\" => org.name,\n        \"teams\" => [\n          {\n            \"name\" => team.name,\n            \"users\" => [\n              {\"name\" => user.name}\n            ]\n          }\n        ])\n    end\n\n    it \"supports mixed only and except at different nesting levels\" do\n      org = Organization.create(name: Faker::Company.name)\n      team = Team.create(name: Faker::Team.name, organization: org)\n      user = User.create(name: Faker::Name.name, email: Faker::Internet.email, team: team)\n\n      # Use only at top level, except at nested level\n      serializer_factory = -> { OrganizationSerializer.new(only: [:teams], except: {teams: {users: [:email]}}) }\n\n      expect(org).to serialized_as(serializer_factory,\n        \"teams\" => [\n          {\n            \"name\" => team.name,\n            \"users\" => [\n              {\"name\" => user.name}\n            ]\n          }\n        ])\n    end\n  end\n\n  context \"invalid filter values\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n    end\n\n    let(:foo_serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name, :address\n      end\n    end\n\n    before { stub_const(\"FooSerializer\", foo_serializer_class) }\n\n    it \"raises error for non-Array/Hash only filters\" do\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      expect do\n        FooSerializer.new(only: \"invalid\").serialize(foo)\n      end.to raise_error(NoMethodError)\n      # TODO: change the error to be ArgumentError\n    end\n\n    it \"raises error for non-Array/Hash except filters\" do\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n      expect do\n        FooSerializer.new(except: 123).serialize(foo)\n      end.to raise_error(NoMethodError)\n      # TODO: change the error to be ArgumentError\n    end\n\n    it \"handles filters on non-existent attributes\" do\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      # Should not raise error, just ignore non-existent attributes\n      expect(foo).to serialized_as(-> { FooSerializer.new(only: [:name, :non_existent]) }, \"name\" => foo.name)\n    end\n\n    it \"handles filters on non-existent associations\" do\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      serializer_factory = -> { FooSerializer.new(only: {non_existent_association: [:name]}) }\n\n      expect(foo).to serialized_as(serializer_factory,\n        \"name\" => foo.name,\n        \"address\" => foo.address)\n    end\n  end\n\n  context \"filters_for method\" do\n    before do\n      Temping.create(:foo) do\n        with_columns do |t|\n          t.string :name\n          t.string :address\n        end\n      end\n    end\n\n    it \"fetches the filters from the serializer\" do\n      class FooWithFiltersForSerializer < Panko::Serializer\n        attributes :name, :address\n\n        def self.filters_for(context, scope)\n          {only: [:name]}\n        end\n      end\n\n      foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n      expect(foo).to serialized_as(-> { FooWithFiltersForSerializer.new }, \"name\" => foo.name)\n    end\n  end\nend\n"
  },
  {
    "path": "spec/features/hash_serialization_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"Hash Serialization\" do\n  class FooSerializer < Panko::Serializer\n    attributes :name, :address\n  end\n\n  it \"serializes hash with string keys\" do\n    foo = {\n      \"name\" => Faker::Lorem.word,\n      \"address\" => Faker::Lorem.word\n    }\n\n    expect(foo).to serialized_as(FooSerializer,\n      \"name\" => foo[\"name\"],\n      \"address\" => foo[\"address\"])\n  end\n\n  it \"serializes HashWithIndifferentAccess with symbol keys\" do\n    foo = ActiveSupport::HashWithIndifferentAccess.new(\n      name: Faker::Lorem.word,\n      address: Faker::Lorem.word\n    )\n\n    expect(foo).to serialized_as(FooSerializer,\n      \"name\" => foo[\"name\"],\n      \"address\" => foo[\"address\"])\n  end\n\n  it \"serializes HashWithIndifferentAccess with string keys\" do\n    foo = ActiveSupport::HashWithIndifferentAccess.new(\n      \"name\" => Faker::Lorem.word,\n      \"address\" => Faker::Lorem.word\n    )\n\n    expect(foo).to serialized_as(FooSerializer,\n      \"name\" => foo[\"name\"],\n      \"address\" => foo[\"address\"])\n  end\nend\n"
  },
  {
    "path": "spec/features/poro_serialization_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe \"PORO Serialization\" do\n  class FooSerializer < Panko::Serializer\n    attributes :name, :address\n  end\n\n  it \"serializes plain objects\" do\n    class PlainFoo\n      attr_accessor :name, :address\n\n      def initialize(name, address)\n        @name = name\n        @address = address\n      end\n    end\n\n    foo = PlainFoo.new(Faker::Lorem.word, Faker::Lorem.word)\n\n    expect(foo).to serialized_as(FooSerializer,\n      \"name\" => foo.name,\n      \"address\" => foo.address)\n  end\nend\n"
  },
  {
    "path": "spec/spec_helper.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nrequire \"logger\"\nrequire \"panko_serializer\"\nrequire \"faker\"\nrequire \"active_record\"\nrequire \"temping\"\n\n# Load database configuration helper\nrequire_relative \"support/database_config\"\n\n# Require database adapters based on environment\ncase DatabaseConfig.database_type\nwhen \"sqlite\"\n  require \"sqlite3\"\nwhen \"postgresql\"\n  require \"pg\"\nwhen \"mysql\"\n  require \"trilogy\"\nend\n\n# Set up database connection\nDatabaseConfig.setup_database\nActiveRecord::Base.establish_connection(DatabaseConfig.config)\n\n# Don't show migration output\nActiveRecord::Migration.verbose = false\n\nRSpec.configure do |config|\n  config.order = \"random\"\n\n  # Enable flags like --only-failures and --next-failure\n  config.example_status_persistence_file_path = \".rspec_status\"\n\n  config.filter_run focus: true\n  config.run_all_when_everything_filtered = true\n\n  config.full_backtrace = ENV.fetch(\"CI\", false)\n\n  if ENV.fetch(\"CI\", false)\n    config.before(:example, :focus) do\n      raise \"This example was committed with `:focus` and should not have been\"\n    end\n  end\n\n  config.expect_with :rspec do |c|\n    c.syntax = :expect\n  end\n\n  config.after do\n    Temping.teardown\n  end\nend\n\nRSpec::Matchers.define :serialized_as do |serializer_factory_or_class, output|\n  serializer_factory = if serializer_factory_or_class.respond_to?(:call)\n    serializer_factory_or_class\n  else\n    -> { serializer_factory_or_class.new }\n  end\n\n  match do |object|\n    expect(serializer_factory.call.serialize(object)).to eq(output)\n\n    json = Oj.load serializer_factory.call.serialize_to_json(object)\n    expect(json).to eq(output)\n  end\n\n  failure_message do |object|\n    <<~FAILURE\n\n      Expected Output:\n      #{output}\n\n      Got:\n\n      Object: #{serializer_factory.call.serialize(object)}\n      JSON: #{Oj.load(serializer_factory.call.serialize_to_json(object))}\n    FAILURE\n  end\nend\n\nif GC.respond_to?(:verify_compaction_references)\n  # This method was added in Ruby 3.0.0. Calling it this way asks the GC to\n  # move objects around, helping to find object movement bugs.\n  GC.verify_compaction_references(double_heap: true, toward: :empty)\nend\n"
  },
  {
    "path": "spec/support/database_config.rb",
    "content": "# frozen_string_literal: true\n\n# Database configuration helper for tests\nclass DatabaseConfig\n  ADAPTERS = {\n    \"sqlite\" => {\n      adapter: \"sqlite3\",\n      database: \":memory:\"\n    },\n    \"postgresql\" => {\n      adapter: \"postgresql\",\n      database: \"panko_test\",\n      host: ENV[\"POSTGRES_HOST\"] || \"localhost\",\n      username: ENV[\"POSTGRES_USER\"] || \"postgres\",\n      password: ENV[\"POSTGRES_PASSWORD\"] || \"\",\n      port: ENV[\"POSTGRES_PORT\"] || 5432\n    },\n    \"mysql\" => {\n      adapter: \"trilogy\",\n      database: \"panko_test\",\n      host: ENV[\"MYSQL_HOST\"] || \"localhost\",\n      username: ENV[\"MYSQL_USER\"] || \"root\",\n      password: ENV[\"MYSQL_PASSWORD\"] || \"\",\n      port: ENV[\"MYSQL_PORT\"] || 3306\n    }\n  }.freeze\n\n  def self.database_type\n    ENV[\"DB\"] || \"sqlite\"\n  end\n\n  def self.config\n    adapter_config = ADAPTERS[database_type]\n    raise \"Unsupported database type: #{database_type}. Supported: #{ADAPTERS.keys.join(\", \")}\" unless adapter_config\n\n    adapter_config\n  end\n\n  def self.setup_database\n    # For CI and local development, we assume databases are already created\n    # SQLite uses in-memory database which needs no setup\n    # PostgreSQL and MySQL databases should be created externally\n    puts \"Using #{database_type} database: #{config[:database]}\" if ENV[\"DEBUG\"]\n  end\n\n  def self.teardown_database\n    # For SQLite in-memory, no teardown needed\n    # For persistent databases, we rely on test transaction rollbacks\n    # rather than dropping/recreating the database for performance\n  end\nend\n"
  },
  {
    "path": "spec/unit/panko/array_serializer_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe Panko::ArraySerializer do\n  describe \"#initialize\" do\n    it \"raises ArgumentError when each_serializer is not provided\" do\n      expect do\n        Panko::ArraySerializer.new([])\n      end.to raise_error(ArgumentError, /Please pass valid each_serializer/)\n    end\n\n    it \"accepts each_serializer option\" do\n      mock_serializer = Class.new(Panko::Serializer)\n\n      expect do\n        Panko::ArraySerializer.new([], each_serializer: mock_serializer)\n      end.not_to raise_error\n    end\n\n    it \"stores subjects\" do\n      mock_serializer = Class.new(Panko::Serializer)\n      subjects = [1, 2, 3]\n\n      array_serializer = Panko::ArraySerializer.new(subjects, each_serializer: mock_serializer)\n\n      expect(array_serializer.subjects).to eq(subjects)\n    end\n\n    it \"builds serialization context from options\" do\n      mock_serializer = Class.new(Panko::Serializer)\n      context = {user_id: 123}\n      scope = \"admin\"\n\n      array_serializer = Panko::ArraySerializer.new([],\n        each_serializer: mock_serializer,\n        context: context,\n        scope: scope)\n\n      serialization_context = array_serializer.instance_variable_get(:@serialization_context)\n      expect(serialization_context.context).to eq(context)\n      expect(serialization_context.scope).to eq(scope)\n    end\n\n    it \"passes filtering options to descriptor\" do\n      mock_serializer = Class.new(Panko::Serializer)\n\n      # Mock SerializationDescriptor.build to verify options are passed\n      expect(Panko::SerializationDescriptor).to receive(:build).with(\n        mock_serializer,\n        hash_including(\n          only: [:name],\n          except: [:email],\n          context: {user_id: 123},\n          scope: \"admin\"\n        ),\n        anything\n      )\n\n      Panko::ArraySerializer.new([],\n        each_serializer: mock_serializer,\n        only: [:name],\n        except: [:email],\n        context: {user_id: 123},\n        scope: \"admin\")\n    end\n\n    it \"defaults only and except to empty arrays when not provided\" do\n      mock_serializer = Class.new(Panko::Serializer)\n\n      expect(Panko::SerializationDescriptor).to receive(:build).with(\n        mock_serializer,\n        hash_including(\n          only: [],\n          except: []\n        ),\n        anything\n      )\n\n      Panko::ArraySerializer.new([], each_serializer: mock_serializer)\n    end\n  end\n\n  describe \"option handling\" do\n    let(:mock_serializer) { Class.new(Panko::Serializer) }\n\n    it \"handles only option\" do\n      array_serializer = Panko::ArraySerializer.new([],\n        each_serializer: mock_serializer,\n        only: [:name, :email])\n\n      # Verify that the descriptor was built with the only option\n      descriptor = array_serializer.instance_variable_get(:@descriptor)\n      expect(descriptor).not_to be_nil\n    end\n\n    it \"handles except option\" do\n      array_serializer = Panko::ArraySerializer.new([],\n        each_serializer: mock_serializer,\n        except: [:password, :secret])\n\n      descriptor = array_serializer.instance_variable_get(:@descriptor)\n      expect(descriptor).not_to be_nil\n    end\n\n    it \"handles context option\" do\n      context = {current_user: \"admin\"}\n      array_serializer = Panko::ArraySerializer.new([],\n        each_serializer: mock_serializer,\n        context: context)\n\n      serialization_context = array_serializer.instance_variable_get(:@serialization_context)\n      expect(serialization_context.context).to eq(context)\n    end\n\n    it \"handles scope option\" do\n      scope = \"public\"\n      array_serializer = Panko::ArraySerializer.new([],\n        each_serializer: mock_serializer,\n        scope: scope)\n\n      serialization_context = array_serializer.instance_variable_get(:@serialization_context)\n      expect(serialization_context.scope).to eq(scope)\n    end\n  end\n\n  describe \"serialization methods\" do\n    let(:mock_serializer) { Class.new(Panko::Serializer) }\n    let(:subjects) { [double(\"obj1\"), double(\"obj2\")] }\n    let(:array_serializer) { Panko::ArraySerializer.new(subjects, each_serializer: mock_serializer) }\n\n    describe \"#serialize\" do\n      it \"calls Panko.serialize_objects with correct parameters\" do\n        mock_writer = double(\"writer\", output: [])\n        allow(Panko::ObjectWriter).to receive(:new).and_return(mock_writer)\n\n        expect(Panko).to receive(:serialize_objects).with(\n          subjects,\n          mock_writer,\n          array_serializer.instance_variable_get(:@descriptor)\n        )\n\n        array_serializer.serialize(subjects)\n      end\n    end\n\n    describe \"#to_a\" do\n      it \"calls serialize_with_writer with stored subjects\" do\n        mock_writer = double(\"writer\", output: [])\n        allow(Panko::ObjectWriter).to receive(:new).and_return(mock_writer)\n\n        expect(Panko).to receive(:serialize_objects).with(\n          subjects,\n          mock_writer,\n          anything\n        )\n\n        array_serializer.to_a\n      end\n    end\n\n    describe \"#serialize_to_json\" do\n      it \"uses Oj::StringWriter for JSON output\" do\n        mock_writer = double(\"writer\", to_s: \"[]\")\n        allow(Oj::StringWriter).to receive(:new).with(mode: :rails).and_return(mock_writer)\n        allow(Panko).to receive(:serialize_objects)\n\n        result = array_serializer.serialize_to_json(subjects)\n        expect(result).to eq(\"[]\")\n      end\n    end\n\n    describe \"#to_json\" do\n      it \"calls serialize_to_json with stored subjects\" do\n        expect(array_serializer).to receive(:serialize_to_json).with(subjects)\n        array_serializer.to_json\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/unit/panko/object_writer_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\nrequire \"active_record/connection_adapters/postgresql_adapter\"\n\ndescribe Panko::ObjectWriter do\n  let(:writer) { Panko::ObjectWriter.new }\n\n  context \"push_object\" do\n    it \"property\" do\n      writer.push_object\n      writer.push_value \"yosi\", \"name\"\n      writer.pop\n\n      expect(writer.output).to eql(\"name\" => \"yosi\")\n    end\n\n    it \"property with separate key value instructions\" do\n      writer.push_object\n      writer.push_key \"name\"\n      writer.push_value \"yosi\"\n      writer.pop\n\n      expect(writer.output).to eql(\"name\" => \"yosi\")\n    end\n\n    it \"supports nested objects\" do\n      writer.push_object\n      writer.push_value \"yosi\", \"name\"\n\n      writer.push_object(\"nested\")\n      writer.push_value \"key1\", \"value\"\n      writer.pop\n\n      writer.pop\n\n      expect(writer.output).to eql(\n        \"name\" => \"yosi\",\n        \"nested\" => {\n          \"value\" => \"key1\"\n        }\n      )\n    end\n\n    it \"supports nested arrays\" do\n      writer.push_object\n      writer.push_value \"yosi\", \"name\"\n\n      writer.push_object(\"nested\")\n      writer.push_value \"key1\", \"value\"\n      writer.pop\n\n      writer.push_array \"values\"\n      writer.push_object\n      writer.push_value \"item\", \"key\"\n      writer.pop\n      writer.pop\n\n      writer.pop\n\n      expect(writer.output).to eql(\n        \"name\" => \"yosi\",\n        \"nested\" => {\n          \"value\" => \"key1\"\n        },\n        \"values\" => [\n          {\"key\" => \"item\"}\n        ]\n      )\n    end\n  end\n\n  it \"supports arrays\" do\n    writer.push_array\n\n    writer.push_object\n    writer.push_value \"key1\", \"value\"\n    writer.pop\n\n    writer.push_object\n    writer.push_value \"key2\", \"value2\"\n    writer.pop\n\n    writer.pop\n\n    expect(writer.output).to eql([\n      {\n        \"value\" => \"key1\"\n      },\n      {\n        \"value2\" => \"key2\"\n      }\n    ])\n  end\nend\n"
  },
  {
    "path": "spec/unit/panko/response_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe Panko::Response do\n  before do\n    Temping.create(:foo) do\n      with_columns do |t|\n        t.string :name\n        t.string :address\n      end\n    end\n  end\n\n  let(:foo_serializer_class) do\n    Class.new(Panko::Serializer) do\n      attributes :name, :address\n    end\n  end\n\n  let(:foo_with_context_serializer_class) do\n    Class.new(Panko::Serializer) do\n      attributes :name, :context_value\n\n      def context_value\n        context[:value]\n      end\n    end\n  end\n\n  before do\n    stub_const(\"FooSerializer\", foo_serializer_class)\n    stub_const(\"FooWithContextSerializer\", foo_with_context_serializer_class)\n  end\n\n  it \"serializes primitive values\" do\n    response = Panko::Response.new(success: true, num: 1)\n\n    json_response = Oj.load(response.to_json)\n\n    expect(json_response[\"success\"]).to eq(true)\n    expect(json_response[\"num\"]).to eq(1)\n  end\n\n  it \"serializes hash values\" do\n    hash = {\"a\" => 1, \"b\" => 2}\n    response = Panko::Response.new(success: true, hash: hash)\n\n    json_response = Oj.load(response.to_json)\n\n    expect(json_response[\"hash\"]).to eq(hash)\n  end\n\n  it \"serializes json wrapped in json value\" do\n    response = Panko::Response.new(success: true, value: Panko::JsonValue.from('{\"a\":1}'))\n\n    json_response = Oj.load(response.to_json)\n\n    expect(json_response[\"success\"]).to eq(true)\n    expect(json_response[\"value\"]).to eq(\"a\" => 1)\n  end\n\n  it \"serializes array serializer\" do\n    foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n    response = Panko::Response.new(success: true,\n      foos: Panko::ArraySerializer.new(Foo.all, each_serializer: FooSerializer))\n\n    json_response = Oj.load(response.to_json)\n\n    expect(json_response[\"success\"]).to eq(true)\n    expect(json_response[\"foos\"]).to eq([\n      \"name\" => foo.name,\n      \"address\" => foo.address\n    ])\n  end\n\n  it \"supports nesting of responses\" do\n    foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n    response = Panko::Response.new(\n      data: Panko::Response.new(\n        data: Panko::Response.new(\n          rows: [\n            Panko::Response.new(\n              foos: Panko::ArraySerializer.new(Foo.all, each_serializer: FooSerializer)\n            )\n          ]\n        )\n      )\n    )\n\n    json_response = Oj.load(response.to_json)\n\n    expect(json_response).to eq(\n      \"data\" => {\n        \"data\" => {\n          \"rows\" => [\n            \"foos\" => [{\n              \"name\" => foo.name,\n              \"address\" => foo.address\n            }]\n          ]\n        }\n      }\n    )\n  end\n\n  it \"supports array\" do\n    response = Panko::Response.new([\n      data: Panko::Response.new(\n        json_data: Panko::JsonValue.from({a: 1}.to_json)\n      )\n    ])\n\n    json_response = Oj.load(response.to_json)\n\n    expect(json_response).to eql([\n      {\"data\" => {\"json_data\" => {\"a\" => 1}}}\n    ])\n  end\n\n  it \"create\" do\n    foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n\n    response = Panko::Response.create do |t|\n      [\n        {\n          data: t.value(\n            json_data: t.json({a: 1}.to_json),\n            foos: t.array_serializer(Foo.all, FooSerializer),\n            foo: t.serializer(Foo.first, FooSerializer)\n          )\n        }\n      ]\n    end\n\n    json_response = Oj.load(response.to_json)\n\n    expect(json_response).to eql([\n      {\"data\" =>\n        {\n          \"json_data\" => {\"a\" => 1},\n          \"foo\" => {\n            \"name\" => foo.name,\n            \"address\" => foo.address\n          },\n          \"foos\" => [{\n            \"name\" => foo.name,\n            \"address\" => foo.address\n          }]\n        }}\n    ])\n  end\n\n  it \"create with context\" do\n    foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)\n    context = {value: Faker::Lorem.word}\n\n    response = Panko::Response.create do |t|\n      [\n        {\n          data: t.value(\n            foos: t.array_serializer(Foo.all, FooWithContextSerializer, context: context),\n            foo: t.serializer(Foo.first, FooWithContextSerializer, context: context)\n          )\n        }\n      ]\n    end\n\n    json_response = Oj.load(response.to_json)\n\n    expect(json_response).to eql([\n      {\"data\" =>\n        {\n          \"foo\" => {\n            \"name\" => foo.name,\n            \"context_value\" => context[:value]\n          },\n          \"foos\" => [{\n            \"name\" => foo.name,\n            \"context_value\" => context[:value]\n          }]\n        }}\n    ])\n  end\nend\n"
  },
  {
    "path": "spec/unit/panko/serialization_descriptor_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe Panko::SerializationDescriptor do\n  class FooSerializer < Panko::Serializer\n    attributes :name, :address\n  end\n\n  context \"attributes\" do\n    it \"simple fields\" do\n      descriptor = Panko::SerializationDescriptor.build(FooSerializer)\n\n      expect(descriptor).not_to be_nil\n      expect(descriptor.attributes).to eq([\n        Panko::Attribute.create(:name),\n        Panko::Attribute.create(:address)\n      ])\n    end\n\n    it \"method attributes\" do\n      class SerializerWithMethodsSerializer < Panko::Serializer\n        attributes :name, :address, :something\n\n        def something\n          \"#{object.name} #{object.address}\"\n        end\n      end\n\n      descriptor = Panko::SerializationDescriptor.build(SerializerWithMethodsSerializer)\n\n      expect(descriptor).not_to be_nil\n      expect(descriptor.attributes).to eq([\n        Panko::Attribute.create(:name),\n        Panko::Attribute.create(:address)\n      ])\n      expect(descriptor.method_fields).to eq([:something])\n    end\n\n    it \"aliases\" do\n      class AttribteAliasesSerializer < Panko::Serializer\n        aliases name: :full_name\n      end\n\n      descriptor = Panko::SerializationDescriptor.build(AttribteAliasesSerializer)\n\n      expect(descriptor).not_to be_nil\n      expect(descriptor.attributes).to eq([\n        Panko::Attribute.create(:name, alias_name: :full_name)\n      ])\n    end\n\n    it \"allows multiple filters in other runs\" do\n      class MultipleFiltersTestSerializer < Panko::Serializer\n        attributes :name, :address\n        has_many :foos, each_serializer: FooSerializer\n      end\n\n      descriptor = Panko::SerializationDescriptor.build(MultipleFiltersTestSerializer, only: {\n        instance: [:foos],\n        foos: [:name]\n      })\n\n      expect(descriptor.has_many_associations.first.descriptor.attributes).to eq([\n        Panko::Attribute.create(:name)\n      ])\n\n      descriptor = Panko::SerializationDescriptor.build(MultipleFiltersTestSerializer)\n\n      expect(descriptor.has_many_associations.first.descriptor.attributes).to eq([\n        Panko::Attribute.create(:name),\n        Panko::Attribute.create(:address)\n      ])\n    end\n  end\n\n  context \"associations\" do\n    it \"has_one: build_descriptor\" do\n      class BuilderTestFooHolderHasOneSerializer < Panko::Serializer\n        attributes :name\n\n        has_one :foo, serializer: FooSerializer\n      end\n\n      descriptor = Panko::SerializationDescriptor.build(BuilderTestFooHolderHasOneSerializer)\n\n      expect(descriptor).not_to be_nil\n      expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)])\n      expect(descriptor.method_fields).to be_empty\n\n      expect(descriptor.has_one_associations.count).to eq(1)\n\n      foo_association = descriptor.has_one_associations.first\n      expect(foo_association.name_sym).to eq(:foo)\n\n      foo_descriptor = Panko::SerializationDescriptor.build(FooSerializer, {})\n      expect(foo_association.descriptor.attributes).to eq(foo_descriptor.attributes)\n      expect(foo_association.descriptor.method_fields).to eq(foo_descriptor.method_fields)\n    end\n\n    it \"has_many: builds descriptor\" do\n      class BuilderTestFoosHasManyHolderSerializer < Panko::Serializer\n        attributes :name\n\n        has_many :foos, serializer: FooSerializer\n      end\n\n      descriptor = Panko::SerializationDescriptor.build(BuilderTestFoosHasManyHolderSerializer)\n\n      expect(descriptor).not_to be_nil\n      expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)])\n      expect(descriptor.method_fields).to be_empty\n      expect(descriptor.has_one_associations).to be_empty\n\n      expect(descriptor.has_many_associations.count).to eq(1)\n\n      foo_association = descriptor.has_many_associations.first\n      expect(foo_association.name_sym).to eq(:foos)\n\n      foo_descriptor = Panko::SerializationDescriptor.build(FooSerializer, {})\n      expect(foo_association.descriptor.attributes).to eq(foo_descriptor.attributes)\n      expect(foo_association.descriptor.method_fields).to eq(foo_descriptor.method_fields)\n    end\n  end\n\n  context \"filter\" do\n    it \"only\" do\n      descriptor = Panko::SerializationDescriptor.build(FooSerializer, only: [:name])\n\n      expect(descriptor).not_to be_nil\n      expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)])\n      expect(descriptor.method_fields).to be_empty\n    end\n\n    it \"except\" do\n      descriptor = Panko::SerializationDescriptor.build(FooSerializer, except: [:name])\n\n      expect(descriptor).not_to be_nil\n      expect(descriptor.attributes).to eq([Panko::Attribute.create(:address)])\n      expect(descriptor.method_fields).to be_empty\n    end\n\n    it \"except filters aliases\" do\n      class ExceptFooWithAliasesSerializer < Panko::Serializer\n        aliases name: :full_name, address: :full_address\n      end\n\n      descriptor = Panko::SerializationDescriptor.build(ExceptFooWithAliasesSerializer, except: [:full_name])\n\n      expect(descriptor).not_to be_nil\n      expect(descriptor.attributes).to eq([Panko::Attribute.create(:address, alias_name: :full_address)])\n    end\n\n    it \"only filters aliases\" do\n      class OnlyFooWithAliasesSerializer < Panko::Serializer\n        attributes :address\n        aliases name: :full_name\n      end\n\n      descriptor = Panko::SerializationDescriptor.build(OnlyFooWithAliasesSerializer, only: [:full_name])\n\n      expect(descriptor).not_to be_nil\n      expect(descriptor.attributes).to eq([Panko::Attribute.create(:name, alias_name: :full_name)])\n    end\n\n    it \"only - filters aliases and fields\" do\n      class OnlyWithFieldsFooWithAliasesSerializer < Panko::Serializer\n        attributes :address, :another_field\n        aliases name: :full_name\n      end\n\n      descriptor = Panko::SerializationDescriptor.build(OnlyWithFieldsFooWithAliasesSerializer, only: %i[full_name address])\n\n      expect(descriptor).not_to be_nil\n      expect(descriptor.attributes).to eq([\n        Panko::Attribute.create(:address),\n        Panko::Attribute.create(:name, alias_name: :full_name)\n      ])\n    end\n\n    it \"filters associations\" do\n      class FooHasOneSerilizers < Panko::Serializer\n        attributes :name\n\n        has_one :foo1, serializer: FooSerializer\n        has_one :foo2, serializer: FooSerializer\n      end\n\n      descriptor = Panko::SerializationDescriptor.build(FooHasOneSerilizers, only: %i[name foo1])\n\n      expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)])\n      expect(descriptor.has_one_associations.count).to eq(1)\n\n      foos_assoc = descriptor.has_one_associations.first\n      expect(foos_assoc.name_sym).to eq(:foo1)\n      expect(foos_assoc.descriptor.attributes).to eq([Panko::Attribute.create(:name), Panko::Attribute.create(:address)])\n    end\n\n    describe \"association filters\" do\n      it \"accepts only as option\" do\n        class AssocFilterTestFoosHolderSerializer < Panko::Serializer\n          attributes :name\n          has_many :foos, serializer: FooSerializer\n        end\n\n        descriptor = Panko::SerializationDescriptor.build(AssocFilterTestFoosHolderSerializer, only: {foos: [:address]})\n\n        expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)])\n        expect(descriptor.has_many_associations.count).to eq(1)\n\n        foos_assoc = descriptor.has_many_associations.first\n        expect(foos_assoc.name_sym).to eq(:foos)\n        expect(foos_assoc.descriptor.attributes).to eq([Panko::Attribute.create(:address)])\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/unit/panko/serializer_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe Panko::Serializer do\n  describe \"class methods\" do\n    describe \".inherited\" do\n      it \"creates a new descriptor for the child class\" do\n        base_class = Class.new(Panko::Serializer)\n        child_class = Class.new(base_class)\n\n        expect(child_class._descriptor).not_to be_nil\n        expect(child_class._descriptor).not_to eq(base_class._descriptor)\n        expect(child_class._descriptor.type).to eq(child_class)\n      end\n\n      it \"duplicates parent descriptor when inheriting\" do\n        base_class = Class.new(Panko::Serializer) do\n          attributes :name\n        end\n\n        child_class = Class.new(base_class)\n\n        expect(child_class._descriptor.attributes.map(&:name)).to include(\"name\")\n        expect(child_class._descriptor).not_to equal(base_class._descriptor)\n      end\n\n      it \"initializes empty collections for new serializers\" do\n        serializer_class = Class.new(Panko::Serializer)\n\n        descriptor = serializer_class._descriptor\n        expect(descriptor.attributes).to eq([])\n        expect(descriptor.aliases).to eq({})\n        expect(descriptor.method_fields).to eq([])\n        expect(descriptor.has_many_associations).to eq([])\n        expect(descriptor.has_one_associations).to eq([])\n      end\n    end\n\n    describe \".attributes\" do\n      it \"adds attributes to the descriptor\" do\n        serializer_class = Class.new(Panko::Serializer) do\n          attributes :name, :email\n        end\n\n        attribute_names = serializer_class._descriptor.attributes.map(&:name)\n        expect(attribute_names).to include(\"name\", \"email\")\n      end\n\n      it \"ensures uniqueness of attributes\" do\n        serializer_class = Class.new(Panko::Serializer) do\n          attributes :name, :email, :name\n        end\n\n        attribute_names = serializer_class._descriptor.attributes.map(&:name)\n        expect(attribute_names.count(\"name\")).to eq(1)\n      end\n    end\n\n    describe \".aliases\" do\n      it \"adds aliased attributes to the descriptor\" do\n        serializer_class = Class.new(Panko::Serializer) do\n          aliases name: :full_name, email: :email_address\n        end\n\n        attributes = serializer_class._descriptor.attributes\n        name_attr = attributes.find { |attr| attr.name == \"name\" }\n        email_attr = attributes.find { |attr| attr.name == \"email\" }\n\n        expect(name_attr.alias_name).to eq(\"full_name\")\n        expect(email_attr.alias_name).to eq(\"email_address\")\n      end\n    end\n\n    describe \".method_added\" do\n      it \"moves attribute to method_fields when method is defined\" do\n        serializer_class = Class.new(Panko::Serializer) do\n          attributes :name, :computed_field\n\n          def computed_field\n            \"computed\"\n          end\n        end\n\n        regular_attributes = serializer_class._descriptor.attributes.map(&:name)\n        method_fields = serializer_class._descriptor.method_fields.map(&:name)\n\n        expect(regular_attributes).to include(\"name\")\n        expect(regular_attributes).not_to include(\"computed_field\")\n        expect(method_fields).to include(\"computed_field\")\n      end\n\n      it \"preserves alias_name when moving to method_fields\" do\n        serializer_class = Class.new(Panko::Serializer) do\n          aliases computed_field: :computed_alias\n\n          def computed_field\n            \"computed\"\n          end\n        end\n\n        method_field = serializer_class._descriptor.method_fields.find { |attr| attr.name == \"computed_field\" }\n        expect(method_field.alias_name).to eq(\"computed_alias\")\n      end\n    end\n\n    describe \".has_one\" do\n      it \"raises error when serializer cannot be found\" do\n        expect do\n          Class.new(Panko::Serializer) do\n            has_one :nonexistent_relation\n          end\n        end.to raise_error(/Can't find serializer/)\n      end\n    end\n\n    describe \".has_many\" do\n      it \"raises error when serializer cannot be found\" do\n        expect do\n          Class.new(Panko::Serializer) do\n            has_many :nonexistent_relations\n          end\n        end.to raise_error(/Can't find serializer/)\n      end\n    end\n  end\n\n  describe \"instance methods\" do\n    let(:serializer_class) do\n      Class.new(Panko::Serializer) do\n        attributes :name\n      end\n    end\n\n    describe \"#initialize\" do\n      it \"creates serialization context from options\" do\n        context = {user_id: 123}\n        scope = \"admin\"\n\n        serializer = serializer_class.new(context: context, scope: scope)\n\n        expect(serializer.context).to eq(context)\n        expect(serializer.scope).to eq(scope)\n      end\n\n      it \"builds descriptor with options and context\" do\n        serializer = serializer_class.new(only: [:name])\n\n        expect(serializer.instance_variable_get(:@descriptor)).not_to be_nil\n        expect(serializer.instance_variable_get(:@used)).to eq(false)\n      end\n\n      it \"skips initialization when _skip_init is true\" do\n        serializer = serializer_class.new(_skip_init: true)\n\n        expect(serializer.instance_variable_get(:@descriptor)).to be_nil\n        expect(serializer.instance_variable_get(:@used)).to be_nil\n      end\n    end\n\n    describe \"#context and #scope\" do\n      it \"returns nil for context and scope when not provided\" do\n        serializer = serializer_class.new\n\n        expect(serializer.context).to be_nil\n        expect(serializer.scope).to be_nil\n      end\n\n      it \"returns provided context and scope\" do\n        context = {user_id: 123}\n        scope = \"admin\"\n\n        serializer = serializer_class.new(context: context, scope: scope)\n\n        expect(serializer.context).to eq(context)\n        expect(serializer.scope).to eq(scope)\n      end\n    end\n\n    describe \"single-use enforcement\" do\n      it \"raises error on second use\" do\n        serializer = serializer_class.new\n        mock_object = double(\"object\")\n\n        # Mock the Panko.serialize_object call to avoid dependencies\n        allow(Panko).to receive(:serialize_object)\n\n        # First call should work\n        expect { serializer.serialize(mock_object) }.not_to raise_error\n\n        # Second call should raise error\n        expect { serializer.serialize(mock_object) }.to raise_error(ArgumentError, \"Panko::Serializer instances are single-use\")\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/unit/serializer_resolver_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\n\ndescribe Panko::SerializerResolver do\n  it \"resolves serializer on singular name\" do\n    class CoolSerializer < Panko::Serializer\n    end\n\n    result = Panko::SerializerResolver.resolve(\"cool\", Object)\n\n    expect(result._descriptor).to be_a(Panko::SerializationDescriptor)\n    expect(result._descriptor.type).to eq(CoolSerializer)\n  end\n\n  it \"resolves serializer on plural name\" do\n    class PersonSerializer < Panko::Serializer\n    end\n\n    result = Panko::SerializerResolver.resolve(\"persons\", Object)\n\n    expect(result._descriptor).to be_a(Panko::SerializationDescriptor)\n    expect(result._descriptor.type).to eq(PersonSerializer)\n  end\n\n  it \"resolves serializer on multiple-word name\" do\n    class MyCoolSerializer < Panko::Serializer\n    end\n\n    result = Panko::SerializerResolver.resolve(\"my_cool\", Object)\n\n    expect(result._descriptor).to be_a(Panko::SerializationDescriptor)\n    expect(result._descriptor.type).to eq(MyCoolSerializer)\n  end\n\n  it \"resolves serializer in namespace first\" do\n    class CoolSerializer < Panko::Serializer\n    end\n\n    module MyApp\n      class CoolSerializer < Panko::Serializer\n      end\n\n      class PersonSerializer < Panko::Serializer\n      end\n    end\n\n    result = Panko::SerializerResolver.resolve(\"cool\", MyApp::PersonSerializer)\n    expect(result._descriptor).to be_a(Panko::SerializationDescriptor)\n    expect(result._descriptor.type).to eq(MyApp::CoolSerializer)\n\n    result = Panko::SerializerResolver.resolve(\"cool\", Panko)\n    expect(result._descriptor).to be_a(Panko::SerializationDescriptor)\n    expect(result._descriptor.type).to eq(CoolSerializer)\n  end\n\n  describe \"errors cases\" do\n    it \"returns nil when the serializer name can't be found\" do\n      expect(Panko::SerializerResolver.resolve(\"nonexistent_model\", Object)).to be_nil\n    end\n\n    it \"returns nil when the serializer is not Panko::Serializer\" do\n      class SomeObjectSerializer\n      end\n\n      expect(Panko::SerializerResolver.resolve(\"some_object\", Object)).to be_nil\n    end\n  end\nend\n"
  },
  {
    "path": "spec/unit/type_cast_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"spec_helper\"\nrequire \"active_record/connection_adapters/postgresql_adapter\"\n\ndef check_if_exists(module_name)\n  mod = (begin\n    module_name.constantize\n  rescue\n    nil\n  end)\n  return true if mod\n  false unless mod\nend\n\ndescribe \"Type Casting\" do\n  describe \"String / Text\" do\n    context \"ActiveRecord::Type::String\" do\n      let(:type) { ActiveRecord::Type::String.new }\n\n      it { expect(Panko._type_cast(type, true)).to eq(\"t\") }\n      it { expect(Panko._type_cast(type, nil)).to be_nil }\n      it { expect(Panko._type_cast(type, false)).to eq(\"f\") }\n      it { expect(Panko._type_cast(type, 123)).to eq(\"123\") }\n      it { expect(Panko._type_cast(type, \"hello world\")).to eq(\"hello world\") }\n    end\n\n    context \"ActiveRecord::Type::Text\" do\n      let(:type) { ActiveRecord::Type::Text.new }\n\n      it { expect(Panko._type_cast(type, true)).to eq(\"t\") }\n      it { expect(Panko._type_cast(type, false)).to eq(\"f\") }\n      it { expect(Panko._type_cast(type, 123)).to eq(\"123\") }\n      it { expect(Panko._type_cast(type, \"hello world\")).to eq(\"hello world\") }\n    end\n\n    # We treat uuid as stirng, there is no need for type cast before serialization\n    context \"ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid\" do\n      let(:type) { ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid.new }\n\n      it { expect(Panko._type_cast(type, \"e67d284b-87b8-445e-a20d-3c76ea353866\")).to eq(\"e67d284b-87b8-445e-a20d-3c76ea353866\") }\n    end\n  end\n\n  describe \"Integer\" do\n    context \"ActiveRecord::Type::Integer\" do\n      let(:type) { ActiveRecord::Type::Integer.new }\n\n      it { expect(Panko._type_cast(type, \"\")).to be_nil }\n      it { expect(Panko._type_cast(type, nil)).to be_nil }\n\n      it { expect(Panko._type_cast(type, 1)).to eq(1) }\n      it { expect(Panko._type_cast(type, \"1\")).to eq(1) }\n      it { expect(Panko._type_cast(type, 1.7)).to eq(1) }\n\n      it { expect(Panko._type_cast(type, true)).to eq(1) }\n      it { expect(Panko._type_cast(type, false)).to eq(0) }\n\n      it { expect(Panko._type_cast(type, [6])).to be_nil }\n      it { expect(Panko._type_cast(type, six: 6)).to be_nil }\n    end\n\n    context \"ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer\", if: check_if_exists(\"ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer\") do\n      let(:type) { ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer.new }\n\n      it { expect(Panko._type_cast(type, \"\")).to be_nil }\n      it { expect(Panko._type_cast(type, nil)).to be_nil }\n\n      it { expect(Panko._type_cast(type, 1)).to eq(1) }\n      it { expect(Panko._type_cast(type, \"1\")).to eq(1) }\n      it { expect(Panko._type_cast(type, 1.7)).to eq(1) }\n\n      it { expect(Panko._type_cast(type, true)).to eq(1) }\n      it { expect(Panko._type_cast(type, false)).to eq(0) }\n\n      it { expect(Panko._type_cast(type, [6])).to be_nil }\n      it { expect(Panko._type_cast(type, six: 6)).to be_nil }\n    end\n  end\n\n  context \"ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json\", if: check_if_exists(\"ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json\") do\n    let(:type) { ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json.new }\n\n    it { expect(Panko._type_cast(type, \"\")).to be_nil }\n    it { expect(Panko._type_cast(type, \"shnitzel\")).to be_nil }\n    it { expect(Panko._type_cast(type, nil)).to be_nil }\n\n    it { expect(Panko._type_cast(type, '{\"a\":1}')).to eq('{\"a\":1}') }\n    it { expect(Panko._type_cast(type, \"[6,12]\")).to eq(\"[6,12]\") }\n\n    it { expect(Panko._type_cast(type, \"a\" => 1)).to eq(\"a\" => 1) }\n    it { expect(Panko._type_cast(type, [6, 12])).to eq([6, 12]) }\n  end\n\n  context \"ActiveRecord::Type::Json\", if: check_if_exists(\"ActiveRecord::Type::Json\") do\n    let(:type) { ActiveRecord::Type::Json.new }\n\n    it { expect(Panko._type_cast(type, \"\")).to be_nil }\n    it { expect(Panko._type_cast(type, \"shnitzel\")).to be_nil }\n    it { expect(Panko._type_cast(type, nil)).to be_nil }\n\n    it { expect(Panko._type_cast(type, '{\"a\":1}')).to eq('{\"a\":1}') }\n    it { expect(Panko._type_cast(type, \"[6,12]\")).to eq(\"[6,12]\") }\n\n    it { expect(Panko._type_cast(type, \"a\" => 1)).to eq(\"a\" => 1) }\n    it { expect(Panko._type_cast(type, [6, 12])).to eq([6, 12]) }\n  end\n\n  context \"ActiveRecord::Type::Boolean\" do\n    let(:type) { ActiveRecord::Type::Boolean.new }\n\n    it { expect(Panko._type_cast(type, \"\")).to be_nil }\n    it { expect(Panko._type_cast(type, nil)).to be_nil }\n\n    it { expect(Panko._type_cast(type, true)).to be_truthy }\n    it { expect(Panko._type_cast(type, 1)).to be_truthy }\n    it { expect(Panko._type_cast(type, \"1\")).to be_truthy }\n    it { expect(Panko._type_cast(type, \"t\")).to be_truthy }\n    it { expect(Panko._type_cast(type, \"T\")).to be_truthy }\n    it { expect(Panko._type_cast(type, \"true\")).to be_truthy }\n    it { expect(Panko._type_cast(type, \"TRUE\")).to be_truthy }\n\n    it { expect(Panko._type_cast(type, false)).to be_falsey }\n    it { expect(Panko._type_cast(type, 0)).to be_falsey }\n    it { expect(Panko._type_cast(type, \"0\")).to be_falsey }\n    it { expect(Panko._type_cast(type, \"f\")).to be_falsey }\n    it { expect(Panko._type_cast(type, \"F\")).to be_falsey }\n    it { expect(Panko._type_cast(type, \"false\")).to be_falsey }\n    it { expect(Panko._type_cast(type, \"FALSE\")).to be_falsey }\n  end\n\n  context \"Time\" do\n    let(:type) { ActiveRecord::Type::DateTime.new }\n    let(:date) { DateTime.new(2017, 3, 4, 12, 45, 23) }\n    let(:utc) { ActiveSupport::TimeZone.new(\"UTC\") }\n\n    it \"ISO8601 strings\" do\n      expect(Panko._type_cast(type, date.in_time_zone(utc).as_json)).to eq(\"2017-03-04T12:45:23.000Z\")\n    end\n\n    it \"two digits after .\" do\n      expect(Panko._type_cast(type, \"2018-09-16 14:51:03.97\")).to eq(\"2018-09-16T14:51:03.970Z\")\n    end\n\n    it \"converts string from datbase to utc time zone\" do\n      time = \"2017-07-10 09:26:40.937392\"\n      seconds = 40 + Rational(937_296, 10**6)\n      result = DateTime.new(2017, 7, 10, 9, 26, seconds).in_time_zone(utc)\n\n      expect(Panko._type_cast(type, time)).to eq(result.as_json)\n    end\n  end\nend\n"
  }
]