├── .clang-format
├── .github
├── dependabot.yml
└── workflows
│ ├── docs.yml
│ ├── lint.yml
│ └── ruby.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Appraisals
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── benchmarks
├── BENCHMARKS.md
├── allocs.rb
├── app.rb
├── benchmarking_support.rb
├── bm_ams_0_10.rb
├── bm_object_writer.rb
├── bm_panko_json.rb
├── bm_panko_object.rb
├── bm_plain_object.rb
├── bm_serialization_descriptor.rb
├── bm_serializer_resolver.rb
├── bm_to_object.rb
├── profile.rb
├── sanity.rb
├── setup.rb
└── type_casts
│ ├── bm_active_record.rb
│ ├── bm_panko.rb
│ └── support.rb
├── docs
├── docs
│ ├── associations.md
│ ├── attributes.md
│ ├── design-choices.md
│ ├── getting-started.md
│ ├── introduction.md
│ ├── performance.md
│ └── response-bag.md
├── docusaurus.config.js
├── package-lock.json
├── package.json
├── sidebars.json
├── src
│ └── css
│ │ └── customTheme.css
└── static
│ ├── .DS_Store
│ ├── CNAME
│ ├── css
│ └── custom.css
│ ├── img
│ ├── favicon.ico
│ ├── oss_logo.png
│ ├── undraw_code_review.svg
│ ├── undraw_monitor.svg
│ ├── undraw_note_list.svg
│ ├── undraw_online.svg
│ ├── undraw_open_source.svg
│ ├── undraw_operating_system.svg
│ ├── undraw_react.svg
│ ├── undraw_tweetstorm.svg
│ └── undraw_youtube_tutorial.svg
│ └── index.html
├── ext
└── panko_serializer
│ ├── attributes_writer
│ ├── active_record.c
│ ├── active_record.h
│ ├── attributes_writer.c
│ ├── attributes_writer.h
│ ├── common.c
│ ├── common.h
│ ├── hash.c
│ ├── hash.h
│ ├── plain.c
│ ├── plain.h
│ └── type_cast
│ │ ├── time_conversion.c
│ │ ├── time_conversion.h
│ │ ├── type_cast.c
│ │ └── type_cast.h
│ ├── common.h
│ ├── extconf.rb
│ ├── panko_serializer.c
│ ├── panko_serializer.h
│ └── serialization_descriptor
│ ├── association.c
│ ├── association.h
│ ├── attribute.c
│ ├── attribute.h
│ ├── serialization_descriptor.c
│ └── serialization_descriptor.h
├── gemfiles
├── 7.0.0.gemfile
├── 7.0.0.gemfile.lock
├── 7.1.0.gemfile
├── 7.1.0.gemfile.lock
├── 7.2.0.gemfile
├── 7.2.0.gemfile.lock
├── 8.0.0.gemfile
└── 8.0.0.gemfile.lock
├── lib
├── panko
│ ├── array_serializer.rb
│ ├── association.rb
│ ├── attribute.rb
│ ├── object_writer.rb
│ ├── response.rb
│ ├── serialization_descriptor.rb
│ ├── serializer.rb
│ ├── serializer_resolver.rb
│ └── version.rb
└── panko_serializer.rb
├── panko_serializer.gemspec
└── spec
├── models.rb
├── panko
├── array_serializer_spec.rb
├── object_writer_spec.rb
├── response_spec.rb
├── serialization_descriptor_spec.rb
├── serializer_resolver_spec.rb
├── serializer_spec.rb
└── type_cast_spec.rb
└── spec_helper.rb
/.clang-format:
--------------------------------------------------------------------------------
1 | ---
2 | Language: Cpp
3 | BasedOnStyle: Google
4 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Docs Publishing
5 |
6 | on:
7 | push:
8 | branches: [ master, docup ]
9 |
10 | jobs:
11 | build:
12 |
13 | runs-on: ubuntu-latest
14 | env:
15 | working_directory: 'docs/'
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Use Node.js ${{ matrix.node-version }}
20 | uses: actions/setup-node@v4
21 | with:
22 | node-version: '20.x'
23 |
24 | - name: Install Dependencies
25 | working-directory: ${{ env.working_directory }}
26 | run: npm install
27 |
28 | - name: Install SSH Client 🔑
29 | uses: webfactory/ssh-agent@v0.6.0
30 | with:
31 | ssh-private-key: ${{ secrets.DEPLOY_KEY }}
32 |
33 | - name: Publish
34 | working-directory: ${{ env.working_directory }}
35 | run: |
36 | git config --global user.email "action@github.com"
37 | git config --global user.name "GitHub Action"
38 | GIT_USER=yosiat CURRENT_BRANCH=master USE_SSH=true npm run deploy
39 |
40 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v4
11 |
12 | - name: Install deps
13 | run: |
14 | sudo apt update -y
15 | sudo apt install -y libsqlite3-dev
16 |
17 | - name: Lint Ruby code
18 | uses: ruby/setup-ruby@v1
19 | with:
20 | ruby-version: 3
21 | bundler-cache: true
22 | - run: |
23 | bundle exec rake rubocop
24 |
25 | - name: Lint C
26 | uses: jidicula/clang-format-action@v4.15.0
27 | with:
28 | clang-format-version: "16"
29 | check-path: "ext/panko_serializer"
30 | fallback-style: "Google"
31 |
--------------------------------------------------------------------------------
/.github/workflows/ruby.yml:
--------------------------------------------------------------------------------
1 | name: Panko Serializer CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | ruby: ["3.1", "3.2", "3.3", "3.4"]
12 | rails: ["7.0.0", "7.1.0", "7.2.0", "8.0.0"]
13 | exclude:
14 | - ruby: 3.1
15 | rails: 8.0.0
16 | - ruby: 3.4
17 | rails: 7.0.0
18 |
19 | steps:
20 | - name: Install deps
21 | run: |
22 | sudo apt update -y
23 | sudo apt install -y libsqlite3-dev
24 |
25 | - uses: actions/checkout@v4
26 | - name: Set up Ruby ${{ matrix.ruby }}
27 | uses: ruby/setup-ruby@v1
28 | with:
29 | ruby-version: ${{ matrix.ruby }}
30 |
31 |
32 | - name: Gems Cache
33 | id: gem-cache
34 | uses: actions/cache@v4
35 | with:
36 | path: vendor/bundle
37 | key: ${{ runner.os }}-${{ matrix.ruby }}-${{ matrix.rails }}-gem
38 | restore-keys: |
39 | ${{ runner.os }}-${{ matrix.ruby }}-${{ matrix.rails }}-gem
40 |
41 | - name: Install gems
42 | env:
43 | BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
44 | run: |
45 | gem install bundler
46 | bundle config set path 'vendor/bundle'
47 | bundle check || bundle install --jobs 4 --retry 3
48 |
49 | - name: Compile & test
50 | env:
51 | BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
52 | run: |
53 | bundle exec rake
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 | /vendor/bundle/
11 | .byebug_history
12 | *.bundle
13 |
14 | # rspec failure tracking
15 | .rspec_status
16 | node_modules
17 |
18 | /docs/build/
19 | .cache/
20 | .docusaurus/
21 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | # We want Exclude directives from different
2 | # config files to get merged, not overwritten
3 | inherit_mode:
4 | merge:
5 | - Exclude
6 |
7 | require:
8 | - standard
9 | - rubocop-performance
10 | - standard-performance
11 | - rubocop-rspec
12 |
13 | inherit_gem:
14 | standard: config/base.yml
15 | standard-performance: config/base.yml
16 |
17 | AllCops:
18 | TargetRubyVersion: 3.1
19 | SuggestExtensions: false
20 | NewCops: disable
21 | Exclude:
22 | - ext/**/*
23 | - gemfiles/**/*
24 |
25 |
26 | Style/FrozenStringLiteralComment:
27 | Enabled: true
28 | EnforcedStyle: always
29 | SafeAutoCorrect: true
30 |
31 | # TODO: need to work on specs.
32 | RSpec:
33 | Enabled: false
34 |
35 | Lint/ConstantDefinitionInBlock:
36 | Exclude:
37 | - spec/**/*
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | appraise "7.0.0" do
4 | gem "sqlite3", "~> 1.4"
5 | gem "activesupport", "~> 7.0.0"
6 | gem "activemodel", "~> 7.0.0"
7 | gem "activerecord", "~> 7.0.0", group: :test
8 | end
9 |
10 | appraise "7.1.0" do
11 | gem "sqlite3", "~> 1.4"
12 | gem "activesupport", "~> 7.1.5"
13 | gem "activemodel", "~> 7.1.5"
14 | gem "activerecord", "~> 7.1.5", group: :test
15 | end
16 |
17 | appraise "7.2.0" do
18 | gem "sqlite3", "~> 1.4"
19 | gem "activesupport", "~> 7.2.0"
20 | gem "activemodel", "~> 7.2.0"
21 | gem "activerecord", "~> 7.2.0", group: :test
22 | end
23 |
24 | appraise "8.0.0" do
25 | gem "sqlite3", ">= 2.1"
26 | gem "activesupport", "~> 8.0.0"
27 | gem "activemodel", "~> 8.0.0"
28 | gem "activerecord", "~> 8.0.0", group: :test
29 | end
30 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | gemspec
6 |
7 | group :benchmarks do
8 | gem "vernier"
9 | gem "stackprof"
10 | gem "pg"
11 |
12 | gem "benchmark-ips"
13 | gem "active_model_serializers", "~> 0.10"
14 | gem "terminal-table"
15 | gem "memory_profiler"
16 | end
17 |
18 | group :test do
19 | gem "faker"
20 | end
21 |
22 | group :development do
23 | gem "byebug"
24 | gem "rake"
25 | gem "rspec", "~> 3.0"
26 | gem "rake-compiler"
27 | end
28 |
29 | group :development, :test do
30 | gem "rubocop"
31 |
32 | gem "standard"
33 | gem "standard-performance"
34 | gem "rubocop-performance"
35 | gem "rubocop-rspec"
36 | end
37 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Yosi Attias
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Panko
2 |
3 | 
4 |
5 | Panko is a library which is inspired by ActiveModelSerializers 0.9 for serializing ActiveRecord/Ruby objects to JSON strings, fast.
6 |
7 | To achieve its [performance](https://panko.dev/performance/):
8 |
9 | * Oj - Panko relies on Oj since it's fast and allows for incremental serialization using `Oj::StringWriter`
10 | * Serialization Descriptor - Panko computes most of the metadata ahead of time, to save time later in serialization.
11 | * Type casting — Panko does type casting by itself, instead of relying on ActiveRecord.
12 |
13 | To dig deeper about the performance choices, read [Design Choices](https://panko.dev/design-choices/).
14 |
15 |
16 | Support
17 | -------
18 |
19 | - [Documentation](https://panko.dev/)
20 | - [Getting Started](https://panko.dev/getting-started/)
21 |
22 | License
23 | -------
24 |
25 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
26 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require "rspec/core/rake_task"
5 | require "json"
6 | require "terminal-table"
7 | require "rake/extensiontask"
8 | require "pty"
9 | require "rubocop/rake_task"
10 |
11 | gem = Gem::Specification.load(File.dirname(__FILE__) + "/panko_serializer.gemspec")
12 |
13 | Rake::ExtensionTask.new("panko_serializer", gem) do |ext|
14 | ext.lib_dir = "lib/panko"
15 | end
16 |
17 | Gem::PackageTask.new(gem) do |pkg|
18 | pkg.need_zip = pkg.need_tar = false
19 | end
20 |
21 | RSpec::Core::RakeTask.new(:spec)
22 | Rake::Task[:spec].prerequisites << :compile
23 | Rake::Task[:compile].prerequisites << :clean
24 |
25 | task default: :spec
26 |
27 | RuboCop::RakeTask.new
28 |
29 | def print_and_flush(str)
30 | print str
31 | $stdout.flush
32 | end
33 |
34 | def run_process(cmd)
35 | puts "> Running #{cmd}"
36 | lines = []
37 | _stderr_reader, stderr_writer = IO.pipe
38 | PTY.spawn(cmd, err: stderr_writer.fileno) do |stdout, stdin, pid|
39 | stdout.each do |line|
40 | print_and_flush "."
41 | lines << line
42 | end
43 | rescue Errno::EIO
44 | # ignore this
45 | end
46 |
47 | lines
48 | rescue PTY::ChildExited
49 | puts "The child process exited! - #{cmd}"
50 | []
51 | end
52 |
53 | def run_benchmarks(files, items_count: 2_300)
54 | headings = ["Benchmark", "ip/s", "allocs/retained"]
55 | files.each do |benchmark_file|
56 | lines = run_process "ITEMS_COUNT=#{items_count} RAILS_ENV=production ruby #{benchmark_file}"
57 | rows = lines.map do |line|
58 | row = JSON.parse(line)
59 | row.values
60 | rescue JSON::ParserError
61 | puts "> [ERROR] Failed running #{benchmark_file} - #{lines.join}"
62 | end
63 |
64 | puts "\n\n"
65 | title = File.basename(benchmark_file, ".rb")
66 | table = Terminal::Table.new title: title, headings: headings, rows: rows
67 | puts table
68 | end
69 | end
70 |
71 | namespace :benchmarks do
72 | desc "All"
73 | task :all do
74 | run_benchmarks Dir[File.join(__dir__, "benchmarks", "**", "bm_*")]
75 | end
76 |
77 | desc "Type Casts"
78 | task :type_casts do
79 | run_benchmarks Dir[File.join(__dir__, "benchmarks", "type_casts", "bm_*")], items_count: 0
80 | end
81 |
82 | desc "Sanity"
83 | task :sanity do
84 | puts Time.now.strftime("%d/%m %H:%M:%S")
85 | puts "=========================="
86 |
87 | run_benchmarks [
88 | File.join(__dir__, "benchmarks", "sanity.rb")
89 | ], items_count: 2300
90 |
91 | puts "\n\n"
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/benchmarks/BENCHMARKS.md:
--------------------------------------------------------------------------------
1 | ## Initial state
2 | ```
3 | Native_Posts_2300 57 ip/s 13802 allocs/op
4 | Native_Posts_50 2956 ip/s 302 allocs/op
5 |
6 | AMS_Simple_Posts_2300 21 ip/s 94309 allocs/op
7 | AMS_Simple_Posts_50 1008 ip/s 2059 allocs/op
8 | AMS_HasOne_Posts_2300 9 ip/s 147209 allocs/op
9 | AMS_HasOne_Posts_50 451 ip/s 3209 allocs/op
10 |
11 | Panko_HasOne_Posts_2300 164 ip/s 2372 allocs/op
12 | Panko_HasOne_Posts_50 6118 ip/s 122 allocs/op
13 | Panko_Reused_HasOne_Posts_2300 178 ip/s 2303 allocs/op
14 | Panko_Reused_HasOne_Posts_50 8203 ip/s 53 allocs/op
15 |
16 | Panko_Simple_Posts_2300 150 ip/s 2372 allocs/op
17 | Panko_Simple_Posts_50 5639 ip/s 122 allocs/op
18 | Panko_Reused_Simple_Posts_2300 180 ip/s 2303 allocs/op
19 | Panko_Reused_Simple_Posts_50 8388 ip/s 53 allocs/op
20 | ```
21 |
22 | ## Refactorings, method call support, combining
23 |
24 | ### class eval
25 | ```
26 | Panko_HasOne_Posts_2300 64 ip/s 9477 allocs/op
27 | Panko_HasOne_Posts_50 2397 ip/s 477 allocs/op
28 | Panko_Reused_HasOne_Posts_2300 70 ip/s 9423 allocs/op
29 | Panko_Reused_HasOne_Posts_50 2596 ip/s 423 allocs/op
30 |
31 | Panko_Simple_Posts_2300 191 ip/s 2472 allocs/op
32 | Panko_Simple_Posts_50 5128 ip/s 222 allocs/op
33 | Panko_Reused_Simple_Posts_2300 180 ip/s 2418 allocs/op
34 | Panko_Reused_Simple_Posts_50 5534 ip/s 168 allocs/op
35 | ```
36 |
37 | ### instance eval
38 | ```
39 | Panko_HasOne_Posts_2300 60 ip/s 9473 allocs/op
40 | Panko_HasOne_Posts_50 2399 ip/s 473 allocs/op
41 | Panko_Reused_HasOne_Posts_2300 66 ip/s 9419 allocs/op
42 | Panko_Reused_HasOne_Posts_50 2582 ip/s 419 allocs/op
43 |
44 | Panko_Simple_Posts_2300 195 ip/s 2470 allocs/op
45 | Panko_Simple_Posts_50 4838 ip/s 220 allocs/op
46 | Panko_Reused_Simple_Posts_2300 196 ip/s 2416 allocs/op
47 | Panko_Reused_Simple_Posts_50 6241 ip/s 166 allocs/op
48 | ```
49 |
--------------------------------------------------------------------------------
/benchmarks/allocs.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "benchmarking_support"
4 | require_relative "app"
5 | require_relative "setup"
6 |
7 | require "memory_profiler"
8 |
9 | class PostFastSerializer < Panko::Serializer
10 | attributes :id, :body, :title, :author_id
11 | end
12 |
13 | def count_allocs(&)
14 | memory_report = MemoryProfiler.report(&)
15 | puts memory_report.pretty_print
16 | end
17 |
18 | posts = Post.all.to_a
19 | merged_options = {}.merge(each_serializer: PostFastSerializer)
20 | posts_array_serializer = Panko::ArraySerializer.new([], merged_options)
21 |
22 | # prints out 18402
23 | count_allocs { posts_array_serializer.serialize_to_json posts }
24 |
--------------------------------------------------------------------------------
/benchmarks/app.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/setup"
4 |
5 | require "active_model"
6 | require "active_record"
7 | require "active_support"
8 | require "active_support/json"
9 |
10 | require "active_model_serializers"
11 | require "panko_serializer"
12 |
--------------------------------------------------------------------------------
/benchmarks/benchmarking_support.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "benchmark/ips"
4 | require "json"
5 | require "memory_profiler"
6 |
7 | module Benchmark
8 | module Runner
9 | def data
10 | posts = Post.all.includes(:author).to_a
11 | posts_50 = posts.first(50).to_a
12 | posts_single = posts.first(1).to_a
13 | {all: posts, small: posts_50, single: posts_single}
14 | end
15 |
16 | def run(label = nil, time: 10, disable_gc: true, warmup: 3, &block)
17 | fail ArgumentError.new, "block should be passed" unless block
18 |
19 | GC.start
20 |
21 | if disable_gc
22 | GC.disable
23 | else
24 | GC.enable
25 | end
26 |
27 | memory_report = MemoryProfiler.report(&block)
28 |
29 | report = Benchmark.ips(time, warmup, true) do |x|
30 | x.report(label) { yield }
31 | end
32 |
33 | results = {
34 | label: label,
35 | ips: ActiveSupport::NumberHelper.number_to_delimited(report.entries.first.ips.round(2)),
36 | allocs: "#{memory_report.total_allocated}/#{memory_report.total_retained}"
37 | }.to_json
38 |
39 | puts results
40 | end
41 | end
42 |
43 | extend Benchmark::Runner
44 | end
45 |
--------------------------------------------------------------------------------
/benchmarks/bm_ams_0_10.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "benchmarking_support"
4 | require_relative "app"
5 | require_relative "setup"
6 |
7 | # disable logging for benchmarks
8 | ActiveModelSerializers.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(File::NULL))
9 |
10 | class AmsAuthorFastSerializer < ActiveModel::Serializer
11 | attributes :id, :name
12 | end
13 |
14 | class AmsPostFastSerializer < ActiveModel::Serializer
15 | attributes :id, :body, :title, :author_id, :created_at
16 | end
17 |
18 | class AmsPostWithHasOneFastSerializer < ActiveModel::Serializer
19 | attributes :id, :body, :title, :author_id
20 |
21 | has_one :author, serializer: AmsAuthorFastSerializer
22 | end
23 |
24 | def benchmark_ams(prefix, serializer, options = {})
25 | merged_options = options.merge(each_serializer: serializer)
26 |
27 | posts = Benchmark.data[:all]
28 |
29 | Benchmark.run("AMS_#{prefix}_Posts_#{posts.count}") do
30 | ActiveModelSerializers::SerializableResource.new(posts, merged_options).to_json
31 | end
32 |
33 | posts_50 = Benchmark.data[:small]
34 |
35 | Benchmark.run("AMS_#{prefix}_Posts_50") do
36 | ActiveModelSerializers::SerializableResource.new(posts_50, merged_options).to_json
37 | end
38 | end
39 |
40 | benchmark_ams "Attributes_Simple", AmsPostFastSerializer
41 | benchmark_ams "Attributes_HasOne", AmsPostWithHasOneFastSerializer
42 | benchmark_ams "Attributes_Except", AmsPostWithHasOneFastSerializer, except: [:title]
43 | benchmark_ams "Attributes_Only", AmsPostWithHasOneFastSerializer, only: [:id, :body, :author_id, :author]
44 |
--------------------------------------------------------------------------------
/benchmarks/bm_object_writer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "benchmarking_support"
4 | require_relative "app"
5 |
6 | Benchmark.run("ObjectWriter_OneProperty_PushValue") do
7 | writer = Panko::ObjectWriter.new
8 |
9 | writer.push_object
10 | writer.push_value "value1", "key1"
11 | writer.pop
12 |
13 | writer.output
14 | end
15 |
16 | Benchmark.run("ObjectWriter_TwoProperty_PushValue") do
17 | writer = Panko::ObjectWriter.new
18 |
19 | writer.push_object
20 | writer.push_value "value1", "key1"
21 | writer.push_value "value2", "key2"
22 | writer.pop
23 |
24 | writer.output
25 | end
26 |
27 | Benchmark.run("ObjectWriter_OneProperty_PushValuePushKey") do
28 | writer = Panko::ObjectWriter.new
29 |
30 | writer.push_object
31 | writer.push_key "key1"
32 | writer.push_value "value1"
33 | writer.pop
34 |
35 | writer.output
36 | end
37 |
38 | Benchmark.run("ObjectWriter_TwoProperty_PushValuePushKey") do
39 | writer = Panko::ObjectWriter.new
40 |
41 | writer.push_object
42 | writer.push_key "key1"
43 | writer.push_value "value1"
44 |
45 | writer.push_key "key2"
46 | writer.push_value "value2"
47 | writer.pop
48 |
49 | writer.output
50 | end
51 |
52 | Benchmark.run("ObjectWriter_NestedObject") do
53 | writer = Panko::ObjectWriter.new
54 |
55 | writer.push_object
56 | writer.push_value "value1", "key1"
57 |
58 | writer.push_object "key2"
59 | writer.push_value "value2", "key2"
60 | writer.pop
61 |
62 | writer.pop
63 |
64 | writer.output
65 | end
66 |
--------------------------------------------------------------------------------
/benchmarks/bm_panko_json.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "benchmarking_support"
4 | require_relative "app"
5 | require_relative "setup"
6 |
7 | class AuthorFastSerializer < Panko::Serializer
8 | attributes :id, :name
9 | end
10 |
11 | class PostFastSerializer < Panko::Serializer
12 | attributes :id, :body, :title, :author_id, :created_at
13 | end
14 |
15 | class PostFastWithJsonSerializer < Panko::Serializer
16 | attributes :id, :body, :title, :author_id, :created_at, :data
17 | end
18 |
19 | class PostFastWithMethodCallSerializer < Panko::Serializer
20 | attributes :id, :body, :title, :author_id, :method_call
21 |
22 | def method_call
23 | object.id * 2
24 | end
25 | end
26 |
27 | class PostWithHasOneFastSerializer < Panko::Serializer
28 | attributes :id, :body, :title, :author_id
29 |
30 | has_one :author, serializer: AuthorFastSerializer
31 | end
32 |
33 | class AuthorWithHasManyFastSerializer < Panko::Serializer
34 | attributes :id, :name
35 |
36 | has_many :posts, serializer: PostFastSerializer
37 | end
38 |
39 | def benchmark(prefix, serializer, options = {})
40 | posts = Benchmark.data[:all]
41 |
42 | merged_options = options.merge(each_serializer: serializer)
43 |
44 | Benchmark.run("Panko_ActiveRecord_#{prefix}_Posts_#{posts.count}") do
45 | Panko::ArraySerializer.new(posts, merged_options).to_json
46 | end
47 |
48 | posts_50 = Benchmark.data[:small]
49 |
50 | Benchmark.run("Panko_ActiveRecord_#{prefix}_Posts_50") do
51 | Panko::ArraySerializer.new(posts_50, merged_options).to_json
52 | end
53 | end
54 |
55 | benchmark "Simple", PostFastSerializer
56 | benchmark "SimpleWithJson", PostFastWithJsonSerializer
57 | benchmark "HasOne", PostWithHasOneFastSerializer
58 | benchmark "SimpleWithMethodCall", PostFastWithMethodCallSerializer
59 | benchmark "Except", PostWithHasOneFastSerializer, except: [:title]
60 | benchmark "Only", PostWithHasOneFastSerializer, only: [:id, :body, :author_id, :author]
61 |
--------------------------------------------------------------------------------
/benchmarks/bm_panko_object.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "benchmarking_support"
4 | require_relative "app"
5 | require_relative "setup"
6 |
7 | class AuthorFastSerializer < Panko::Serializer
8 | attributes :id, :name
9 | end
10 |
11 | class PostFastSerializer < Panko::Serializer
12 | attributes :id, :body, :title, :author_id
13 | end
14 |
15 | class PostWithHasOneFastSerializer < Panko::Serializer
16 | attributes :id, :body, :title, :author_id
17 |
18 | has_one :author, serializer: AuthorFastSerializer
19 | end
20 |
21 | class AuthorWithHasManyFastSerializer < Panko::Serializer
22 | attributes :id, :name
23 |
24 | has_many :posts, serializer: PostFastSerializer
25 | end
26 |
27 | def benchmark(prefix, serializer, options = {})
28 | posts = Benchmark.data[:all]
29 |
30 | merged_options = options.merge(each_serializer: serializer)
31 |
32 | Benchmark.run("Panko_ActiveRecord_#{prefix}_Posts_#{posts.count}") do
33 | Panko::ArraySerializer.new(posts, merged_options).to_a
34 | end
35 |
36 | posts_50 = Benchmark.data[:small]
37 |
38 | Benchmark.run("Panko_ActiveRecord_#{prefix}_Posts_50") do
39 | Panko::ArraySerializer.new(posts_50, merged_options).to_a
40 | end
41 | end
42 |
43 | benchmark "Simple", PostFastSerializer
44 | benchmark "HasOne", PostWithHasOneFastSerializer
45 | benchmark "Except", PostWithHasOneFastSerializer, except: [:title]
46 | benchmark "Only", PostWithHasOneFastSerializer, only: [:id, :body, :author_id, :author]
47 |
--------------------------------------------------------------------------------
/benchmarks/bm_plain_object.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "benchmarking_support"
4 | require_relative "app"
5 |
6 | class AuthorFastSerializer < Panko::Serializer
7 | attributes :id, :name
8 | end
9 |
10 | class PostFastSerializer < Panko::Serializer
11 | attributes :id, :body, :title, :author_id, :created_at
12 | end
13 |
14 | class PostFastWithMethodCallSerializer < Panko::Serializer
15 | attributes :id, :body, :title, :author_id, :method_call
16 |
17 | def method_call
18 | object.id * 2
19 | end
20 | end
21 |
22 | class PostWithHasOneFastSerializer < Panko::Serializer
23 | attributes :id, :body, :title, :author_id
24 |
25 | has_one :author, serializer: AuthorFastSerializer
26 | end
27 |
28 | class AuthorWithHasManyFastSerializer < Panko::Serializer
29 | attributes :id, :name
30 |
31 | has_many :posts, serializer: PostFastSerializer
32 | end
33 |
34 | class Post
35 | attr_accessor :id, :body, :title, :created_at, :author_id
36 | attr_reader :author
37 |
38 | def self.create(attrs)
39 | p = Post.new
40 | attrs.each do |k, v|
41 | p.send :"#{k}=", v
42 | end
43 | p
44 | end
45 |
46 | def author=(author)
47 | @author = author
48 | @author_id = author.id
49 | end
50 | end
51 |
52 | class Author
53 | attr_accessor :id, :name
54 |
55 | def self.create(attrs)
56 | a = Author.new
57 | attrs.each do |k, v|
58 | a.send :"#{k}=", v
59 | end
60 | a
61 | end
62 | end
63 |
64 | def benchmark_data
65 | posts = []
66 | ENV.fetch("ITEMS_COUNT", "2300").to_i.times do |i|
67 | posts << Post.create(
68 | id: i,
69 | body: "something about how password restrictions are evil, and less secure, and with the math to prove it.",
70 | title: "Your bank is does not know how to do security",
71 | author: Author.create(id: i, name: "Preston Sego")
72 | )
73 | end
74 |
75 | {all: posts, small: posts.first(50)}
76 | end
77 |
78 | def benchmark(prefix, serializer, options = {})
79 | data = benchmark_data
80 | posts = data[:all]
81 |
82 | merged_options = options.merge(each_serializer: serializer)
83 |
84 | Benchmark.run("Panko_Plain_#{prefix}_Posts_#{posts.count}") do
85 | Panko::ArraySerializer.new(posts, merged_options).to_json
86 | end
87 |
88 | posts_50 = benchmark_data[:small]
89 |
90 | Benchmark.run("Panko_Plain_#{prefix}_Posts_50") do
91 | Panko::ArraySerializer.new(posts_50, merged_options).to_json
92 | end
93 | end
94 |
95 | benchmark "Simple", PostFastSerializer
96 | benchmark "HasOne", PostWithHasOneFastSerializer
97 | benchmark "SimpleWithMethodCall", PostFastWithMethodCallSerializer
98 | benchmark "Except", PostWithHasOneFastSerializer, except: [:title]
99 | benchmark "Only", PostWithHasOneFastSerializer, only: [:id, :body, :author_id, :author]
100 |
--------------------------------------------------------------------------------
/benchmarks/bm_serialization_descriptor.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "benchmarking_support"
4 | require "active_support"
5 |
6 | require "panko_serializer"
7 |
8 | def generate_attributes(count)
9 | (1..count).map { |i| :"attr_#{i}" }
10 | end
11 |
12 | class LeafASerializer < Panko::Serializer
13 | attributes(*generate_attributes(5))
14 | end
15 |
16 | class LeafBSerializer < Panko::Serializer
17 | attributes(*generate_attributes(6))
18 | end
19 |
20 | class ChildrenSerializer < Panko::Serializer
21 | attributes(*generate_attributes(28))
22 |
23 | has_one :leaf_a, serializer: LeafASerializer
24 | has_one :leaf_b, serializer: LeafBSerializer
25 |
26 | def attr_1
27 | end
28 |
29 | def attr_2
30 | end
31 |
32 | def attr_3
33 | end
34 |
35 | def attr_4
36 | end
37 |
38 | def attr_5
39 | end
40 | end
41 |
42 | class ParentSerializer < Panko::Serializer
43 | attributes(*generate_attributes(46))
44 |
45 | has_many :children, serializer: ChildrenSerializer
46 |
47 | def attr_1
48 | end
49 |
50 | def attr_2
51 | end
52 |
53 | def attr_3
54 | end
55 |
56 | def attr_4
57 | end
58 | end
59 |
60 | attrs = generate_attributes(21)
61 | attrs << :children
62 | filters = {
63 | instance: attrs,
64 | children: generate_attributes(11)
65 | }
66 | Benchmark.run("NoFilters") do
67 | Panko::SerializationDescriptor.build(ParentSerializer)
68 | end
69 |
70 | Benchmark.run("Attribute") do
71 | Panko::SerializationDescriptor.build(ParentSerializer, only: [:children])
72 | end
73 |
74 | Benchmark.run("AssociationFilters") do
75 | Panko::SerializationDescriptor.build(ParentSerializer, only: filters)
76 | end
77 |
--------------------------------------------------------------------------------
/benchmarks/bm_serializer_resolver.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "benchmarking_support"
4 | require_relative "app"
5 |
6 | class NotSerializer
7 | end
8 |
9 | class RealSerializer < Panko::Serializer
10 | end
11 |
12 | Benchmark.run("CantFindConst") do
13 | Panko::SerializerResolver.resolve("cant_find_const", Object)
14 | end
15 |
16 | Benchmark.run("NotSerializer") do
17 | Panko::SerializerResolver.resolve("not", Object)
18 | end
19 |
20 | Benchmark.run("RealSerializer") do
21 | Panko::SerializerResolver.resolve("real", Object)
22 | end
23 |
--------------------------------------------------------------------------------
/benchmarks/bm_to_object.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "benchmarking_support"
4 | require_relative "app"
5 | require_relative "setup"
6 |
7 | class PostWithAliasModel < ActiveRecord::Base
8 | self.table_name = "posts"
9 |
10 | alias_attribute :new_id, :id
11 | alias_attribute :new_body, :body
12 | alias_attribute :new_title, :title
13 | alias_attribute :new_author_id, :author_id
14 | alias_attribute :new_created_at, :created_at
15 | end
16 |
17 | class PostWithAliasFastSerializer < Panko::Serializer
18 | attributes :new_id, :new_body, :new_title, :new_author_id, :new_created_at
19 | end
20 |
21 | def benchmark_aliased(prefix, serializer, options = {})
22 | posts = PostWithAliasModel.all.to_a
23 | posts_50 = posts.first(50).to_a
24 |
25 | merged_options = options.merge(each_serializer: serializer)
26 |
27 | Benchmark.run("Panko_#{prefix}_PostWithAliasModels_#{posts.count}") do
28 | Panko::ArraySerializer.new(posts, merged_options).to_json
29 | end
30 |
31 | Benchmark.run("Panko_#{prefix}_Posts_50") do
32 | Panko::ArraySerializer.new(posts_50, merged_options).to_json
33 | end
34 | end
35 |
36 | class AuthorFastSerializer < Panko::Serializer
37 | attributes :id, :name
38 | end
39 |
40 | class PostFastSerializer < Panko::Serializer
41 | attributes :id, :body, :title, :author_id, :created_at
42 | end
43 |
44 | class PostFastWithMethodCallSerializer < Panko::Serializer
45 | attributes :id, :body, :title, :author_id, :created_at, :method_call
46 |
47 | def method_call
48 | object.id
49 | end
50 | end
51 |
52 | class PostWithHasOneFastSerializer < Panko::Serializer
53 | attributes :id, :body, :title, :author_id, :created_at
54 |
55 | has_one :author, serializer: AuthorFastSerializer
56 | end
57 |
58 | class AuthorWithHasManyFastSerializer < Panko::Serializer
59 | attributes :id, :name
60 |
61 | has_many :posts, serializer: PostFastSerializer
62 | end
63 |
64 | def benchmark(prefix, serializer, options = {})
65 | posts = Benchmark.data[:all]
66 |
67 | merged_options = options.merge(each_serializer: serializer)
68 |
69 | Benchmark.run("Panko_#{prefix}_Posts_#{posts.count}") do
70 | Panko::ArraySerializer.new(posts, merged_options).to_a
71 | end
72 |
73 | posts_50 = Benchmark.data[:small]
74 |
75 | Benchmark.run("Panko_#{prefix}_Posts_50") do
76 | Panko::ArraySerializer.new(posts_50, merged_options).to_a
77 | end
78 | end
79 |
80 | benchmark "Simple", PostFastSerializer
81 | benchmark "HasOne", PostWithHasOneFastSerializer
82 |
--------------------------------------------------------------------------------
/benchmarks/profile.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "benchmarking_support"
4 | require_relative "app"
5 | require_relative "setup"
6 |
7 | require "memory_profiler"
8 |
9 | class NullLogger < Logger
10 | def initialize(*args)
11 | end
12 |
13 | def add(*args, &block)
14 | end
15 | end
16 |
17 | class BenchmarkApp < Rails::Application
18 | routes.append do
19 | get "/simple" => "main#simple"
20 | get "/text" => "main#text"
21 |
22 | get "/serialize_to_string" => "main#serialize_to_string"
23 | get "/serialize_to_stream" => "streaming#serialize_to_stream"
24 | end
25 |
26 | config.secret_token = "s" * 30
27 | config.secret_key_base = "foo"
28 | config.consider_all_requests_local = false
29 |
30 | # simulate production
31 | config.cache_classes = true
32 | config.eager_load = true
33 | config.action_controller.perform_caching = true
34 |
35 | # otherwise deadlock occured
36 | config.middleware.delete "Rack::Lock"
37 |
38 | # to disable log files
39 | config.logger = NullLogger.new
40 | config.active_support.deprecation = :log
41 | end
42 |
43 | BenchmarkApp.initialize!
44 |
45 | class AuthorFastSerializer < Panko::Serializer
46 | attributes :id, :name
47 | end
48 |
49 | class PostWithHasOneFastSerializer < Panko::Serializer
50 | attributes :id, :body, :title, :author_id
51 |
52 | has_one :author, serializer: AuthorFastSerializer
53 | end
54 |
55 | class StreamingController < ActionController::Base
56 | include ActionController::Live
57 |
58 | def serialize_to_stream
59 | headers["Content-Type"] = "application/json"
60 |
61 | data = Benchmark.data[:all]
62 | serializer = Panko::ArraySerializer.new([], each_serializer: PostWithHasOneFastSerializer)
63 | writer = Oj::StreamWriter.new(response.stream, mode: :rails)
64 |
65 | serializer.serialize_to_writer(data, writer)
66 |
67 | response.stream.close
68 | end
69 | end
70 |
71 | class RouteNotFoundError < StandardError; end
72 |
73 | def request(method, path)
74 | response = Rack::MockRequest.new(BenchmarkApp).send(method, path)
75 | if response.status.in?([404, 500])
76 | raise RouteNotFoundError.new, "not found #{method.to_s.upcase} #{path}"
77 | end
78 | response
79 | end
80 |
81 | def memory(&)
82 | mem = MemoryProfiler.report(&)
83 | mem.pretty_print
84 | end
85 |
86 | memory { request(:get, "/serialize_to_stream") }
87 |
--------------------------------------------------------------------------------
/benchmarks/sanity.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "benchmarking_support"
4 | require_relative "app"
5 | require_relative "setup"
6 |
7 | class PostWithAliasModel < ActiveRecord::Base
8 | self.table_name = "posts"
9 |
10 | alias_attribute :new_id, :id
11 | alias_attribute :new_body, :body
12 | alias_attribute :new_title, :title
13 | alias_attribute :new_author_id, :author_id
14 | alias_attribute :new_created_at, :created_at
15 | end
16 |
17 | class PostWithAliasFastSerializer < Panko::Serializer
18 | attributes :new_id, :new_body, :new_title, :new_author_id, :new_created_at
19 | end
20 |
21 | def benchmark_aliased(prefix, serializer, options = {})
22 | posts = PostWithAliasModel.all.to_a
23 | posts_50 = posts.first(50).to_a
24 |
25 | merged_options = options.merge(each_serializer: serializer)
26 |
27 | Benchmark.run("Panko_#{prefix}_PostWithAliasModels_#{posts.count}") do
28 | Panko::ArraySerializer.new(posts, merged_options).to_json
29 | end
30 |
31 | Benchmark.run("Panko_#{prefix}_Posts_50") do
32 | Panko::ArraySerializer.new(posts_50, merged_options).to_json
33 | end
34 | end
35 |
36 | class AuthorFastSerializer < Panko::Serializer
37 | attributes :id, :name
38 | end
39 |
40 | class PostFastSerializer < Panko::Serializer
41 | attributes :id, :body, :title, :author_id, :created_at
42 | end
43 |
44 | class PostFastWithMethodCallSerializer < Panko::Serializer
45 | attributes :id, :body, :title, :author_id, :created_at, :method_call
46 |
47 | def method_call
48 | object.id
49 | end
50 | end
51 |
52 | class PostWithHasOneFastSerializer < Panko::Serializer
53 | attributes :id, :body, :title, :author_id, :created_at
54 |
55 | has_one :author, serializer: AuthorFastSerializer
56 | end
57 |
58 | class AuthorWithHasManyFastSerializer < Panko::Serializer
59 | attributes :id, :name
60 |
61 | has_many :posts, serializer: PostFastSerializer
62 | end
63 |
64 | def benchmark(prefix, serializer, options = {})
65 | posts = Benchmark.data[:all]
66 |
67 | merged_options = options.merge(each_serializer: serializer)
68 |
69 | Benchmark.run("Panko_#{prefix}_Posts_#{posts.count}") do
70 | Panko::ArraySerializer.new(posts, merged_options).to_json
71 | end
72 |
73 | posts_50 = Benchmark.data[:small]
74 |
75 | Benchmark.run("Panko_#{prefix}_Posts_50") do
76 | Panko::ArraySerializer.new(posts_50, merged_options).to_json
77 | end
78 | end
79 |
80 | benchmark "Simple", PostFastSerializer
81 | benchmark "HasOne", PostWithHasOneFastSerializer
82 | benchmark "SimpleWithMethodCall", PostFastWithMethodCallSerializer
83 | benchmark_aliased "Simple (aliased)", PostWithAliasFastSerializer
84 |
--------------------------------------------------------------------------------
/benchmarks/setup.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ###########################################
4 | # Setup active record models
5 | ##########################################
6 | require "active_record"
7 | require "sqlite3"
8 | require "securerandom"
9 |
10 | # Change the following to reflect your database settings
11 | ActiveRecord::Base.establish_connection(
12 | adapter: "sqlite3",
13 | database: ":memory:"
14 | )
15 |
16 | # Don't show migration output when constructing fake db
17 | ActiveRecord::Migration.verbose = false
18 |
19 | ActiveRecord::Schema.define do
20 | create_table :authors, force: true do |t|
21 | t.string :name
22 | t.timestamps(null: false)
23 | end
24 |
25 | create_table :posts, force: true do |t|
26 | t.text :body
27 | t.string :title
28 | t.references :author
29 | t.json :data
30 | t.timestamps(null: false)
31 | end
32 | end
33 |
34 | class Author < ActiveRecord::Base
35 | has_one :profile
36 | has_many :posts
37 | end
38 |
39 | class Post < ActiveRecord::Base
40 | belongs_to :author
41 | end
42 |
43 | Post.destroy_all
44 | Author.destroy_all
45 |
46 | # Build out the data to serialize
47 | Post.transaction do
48 | ENV.fetch("ITEMS_COUNT", "2300").to_i.times do
49 | Post.create(
50 | body: SecureRandom.hex(30),
51 | title: SecureRandom.hex(20),
52 | author: Author.create(name: SecureRandom.alphanumeric),
53 | data: {a: 1, b: 2, c: 3}
54 | )
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/benchmarks/type_casts/bm_active_record.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "support"
4 |
5 | def ar_type_convert(type_klass, from, to)
6 | converter = type_klass.new
7 |
8 | assert type_klass.name, converter.deserialize(from), to
9 |
10 | Benchmark.run("#{type_klass.name}_TypeCast") do
11 | converter.deserialize(from)
12 | end
13 |
14 | Benchmark.run("#{type_klass.name}_NoTypeCast") do
15 | converter.deserialize(to)
16 | end
17 | end
18 |
19 | def utc_ar_time
20 | date = DateTime.new(2017, 3, 4, 12, 45, 23)
21 | tz = ActiveSupport::TimeZone.new("UTC")
22 | from = date.in_time_zone(tz).iso8601
23 |
24 | type = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime.new
25 | converter = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type)
26 |
27 | Benchmark.run("#{tz}_#{type.class.name}_TypeCast") do
28 | converter.deserialize(from).iso8601
29 | end
30 | end
31 |
32 | def db_ar_time
33 | type = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime.new
34 | converter = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type)
35 |
36 | from = "2017-07-10 09:26:40.937392"
37 |
38 | Benchmark.run("ActiveRecord_Time_TypeCast_WithISO8601") do
39 | converter.deserialize(from).iso8601
40 | end
41 | end
42 |
43 | ar_type_convert ActiveRecord::Type::String, 1, "1"
44 | ar_type_convert ActiveRecord::Type::Text, 1, "1"
45 | ar_type_convert ActiveRecord::Type::Integer, "1", 1
46 | ar_type_convert ActiveRecord::Type::Float, "1.23", 1.23
47 | ar_type_convert ActiveRecord::Type::Boolean, "true", true
48 | ar_type_convert ActiveRecord::Type::Boolean, "t", true
49 |
50 | if check_if_exists "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json"
51 | ar_type_convert ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json, '{"a":1}', {a: 1}
52 | end
53 | if check_if_exists "ActiveRecord::Type::Json"
54 | ar_type_convert ActiveRecord::Type::Json, '{"a":1}', {a: 1}
55 | end
56 |
57 | db_ar_time
58 | utc_ar_time
59 |
--------------------------------------------------------------------------------
/benchmarks/type_casts/bm_panko.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "support"
4 |
5 | def panko_type_convert(type_klass, from, to)
6 | converter = type_klass.new
7 | assert type_klass.name.to_s, Panko._type_cast(converter, from), to
8 |
9 | Benchmark.run("#{type_klass.name}_TypeCast") do
10 | Panko._type_cast(converter, from)
11 | end
12 |
13 | Benchmark.run("#{type_klass.name}_NoTypeCast") do
14 | Panko._type_cast(converter, to)
15 | end
16 | end
17 |
18 | def utc_panko_time
19 | date = DateTime.new(2017, 3, 4, 12, 45, 23)
20 | tz = ActiveSupport::TimeZone.new("UTC")
21 | from = date.in_time_zone(tz).iso8601
22 |
23 | type = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime.new
24 | converter = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type)
25 |
26 | to = Panko._type_cast(converter, from)
27 |
28 | Benchmark.run("#{tz}_#{type.class.name}_TypeCast") do
29 | Panko._type_cast(converter, from)
30 | end
31 |
32 | Benchmark.run("#{tz}_#{type.class.name}_NoTypeCast") do
33 | Panko._type_cast(converter, to)
34 | end
35 | end
36 |
37 | def db_panko_time
38 | type = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime.new
39 | converter = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type)
40 |
41 | from = "2017-07-10 09:26:40.937392"
42 |
43 | Benchmark.run("Panko_Time_TypeCast") do
44 | Panko._type_cast(converter, from)
45 | end
46 | end
47 |
48 | panko_type_convert ActiveRecord::Type::String, 1, "1"
49 | panko_type_convert ActiveRecord::Type::Text, 1, "1"
50 | panko_type_convert ActiveRecord::Type::Integer, "1", 1
51 | panko_type_convert ActiveRecord::Type::Float, "1.23", 1.23
52 | panko_type_convert ActiveRecord::Type::Boolean, "true", true
53 | panko_type_convert ActiveRecord::Type::Boolean, "t", true
54 |
55 | if check_if_exists "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json"
56 | panko_type_convert ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json, '{"a":1}', '{"a":1}'
57 | end
58 | if check_if_exists "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb"
59 | panko_type_convert ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb, '{"a":1}', '{"a":1}'
60 | end
61 | if check_if_exists "ActiveRecord::Type::Json"
62 | panko_type_convert ActiveRecord::Type::Json, '{"a":1}', '{"a":1}'
63 | end
64 | db_panko_time
65 | utc_panko_time
66 |
--------------------------------------------------------------------------------
/benchmarks/type_casts/support.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "active_record"
4 | require "active_record/connection_adapters/postgresql_adapter"
5 | require "active_model"
6 | require "active_support/all"
7 | require "pg"
8 | require "oj"
9 |
10 | require_relative "../benchmarking_support"
11 | require_relative "../../lib/panko_serializer"
12 |
13 | def assert(type_name, from, to)
14 | raise "#{type_name} - #{from.class} is not equals to #{to.class}" unless from.to_json == to.to_json
15 | end
16 |
17 | def check_if_exists(module_name)
18 | mod = begin
19 | module_name.constantize
20 | rescue
21 | nil
22 | end
23 | return true if mod
24 | false unless mod
25 | end
26 |
27 | Time.zone = "UTC"
28 |
--------------------------------------------------------------------------------
/docs/docs/associations.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: associations
3 | title: Associations
4 | sidebar_label: Associations
5 | ---
6 | A serializer can define it's own associations - both `has_many` and `has_one` to serialize under the context of the object.
7 |
8 | For example:
9 |
10 | ```ruby
11 |
12 | class PostSerializer < Panko::Serializer
13 | attributes :title, :body
14 |
15 | has_one :author, serializer: AuthorSerializer
16 | has_many :comments, each_serializer: CommentSerializer
17 | end
18 |
19 | ```
20 |
21 | ### Associations with aliases
22 |
23 | An association key name can be aliased with the `name` option.
24 |
25 | For example:
26 | the `actual_author` property will be converted to `alias_author`.
27 |
28 | ```ruby
29 |
30 | class PostSerializer < Panko::Serializer
31 | attributes :title, :body
32 |
33 | has_one :actual_author, serializer: AuthorSerializer, name: :alias_author
34 | has_many :comments, each_serializer: CommentSerializer
35 | end
36 |
37 | ```
38 |
39 | ### Inference
40 |
41 | Panko can find the type of the serializer by looking at the relationship name, so instead of specifying
42 | the serializer at the above example, we can:
43 |
44 | ```ruby
45 |
46 | class PostSerializer < Panko::Serializer
47 | attributes :title, :body
48 |
49 | has_one :author
50 | has_many :comments
51 | end
52 |
53 | ```
54 |
55 | The logic of inferencing is:
56 |
57 | - Take the name of the relationship (for example - `:author` / `:comments`) singularize and camelize it.
58 | - Look for const defined with the name above and "Serializer" suffix (by using `Object.const_get`).
59 |
60 | > If Panko can't find the serializer it will throw an error on startup time, for example: `Can't find serializer for PostSerializer.author has_one relationship`.
61 |
62 | ## Nested Filters
63 |
64 | As talked before, Panko allows you to filter the attributes of a serializer.
65 | But Panko lets you take that step further, and filters the attributes of you associations so you can re-use your serializers in your application.
66 |
67 | For example, let's say one portion of the application needs to serialize a list of posts but only with their - `title`, `body`, author's id and comments id.
68 |
69 | We can declare tailored serializer for this, or we can re-use the above defined serializer - `PostSerializer` and use nested filters.
70 |
71 | ```ruby
72 |
73 | posts = Post.all
74 |
75 | Panko::ArraySerializer.new(posts, each_serializer: PostSerializer, only: {
76 | instance: [:title, :body, :author, :comments],
77 | author: [:id],
78 | comments: [:id],
79 | })
80 |
81 | ```
82 |
83 | Let's dissect the `only` option we passed:
84 |
85 | - `instance` - list of attributes (and associations) we want to serialize for the current instance of the serializer, in this case - `PostSerializer`.
86 | - `author`, `comments` - here we specify the list of attributes we want to serialize for each association.
87 |
88 | It's important to note that Nested Filters are recursive, in other words, we can filter the association's associations.
89 |
90 | For example, `CommentSerializer` has an `has_one` association `Author`, and for each `comments.author` we can only serialize it's name.
91 |
92 | ```ruby
93 |
94 | posts = Post.all
95 |
96 | Panko::ArraySerializer.new(posts, only: {
97 | instance: [:title, :body, :author, :comments],
98 | author: [:id],
99 | comments: {
100 | instance: [:id, :author],
101 | author: [:name]
102 | }
103 | })
104 |
105 | ```
106 |
107 | As you see now in `comments` the `instance` have different meaning, the `CommentSerializer`.
108 |
--------------------------------------------------------------------------------
/docs/docs/attributes.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: attributes
3 | title: Attributes
4 | sidebar_label: Attributes
5 | ---
6 | Attributes allow you to specify which record attributes you want to serialize.
7 |
8 | There are two types of attributes:
9 |
10 | - Field - simple columns defined on the record it self.
11 | - Virtual/Method - this allows to include properties beyond simple fields.
12 |
13 | ```ruby
14 |
15 | class UserSerializer < Panko::Serializer
16 | attributes :full_name
17 |
18 | def full_name
19 | "#{object.first_name} #{object.last_name}"
20 | end
21 | end
22 |
23 | ```
24 |
25 | ## Field Attributes
26 |
27 | Using field attributes you can control which columns of the given ActiveRecord object you want to serialize.
28 |
29 | Instead of relying on ActiveRecord to do it's type casting, Panko does on it's own for performance reasons (read more in [Design Choices](design-choices.md#type-casting)).
30 |
31 | ## Method Attributes
32 |
33 | Method attributes are used when your serialized values can be derived from the object you are serializing.
34 |
35 | The serializer's attribute methods can access the object being serialized as `object`:
36 |
37 | ```ruby
38 |
39 | class PostSerializer < Panko::Serializer
40 | attributes :author_name
41 |
42 | def author_name
43 | "#{object.author.first_name} #{object.author.last_name}"
44 | end
45 | end
46 |
47 | ```
48 |
49 | Another useful thing you can pass your serializer is `context`, a `context` is a bag of data whom your serializer may need.
50 |
51 | For example, here we will pass feature flags:
52 |
53 | ```ruby
54 |
55 | class UserSerializer < Panko::Serializer
56 | attributes :id, :email
57 |
58 | def feature_flags
59 | context[:feature_flags]
60 | end
61 | end
62 |
63 | serializer = UserSerializer.new(context: {
64 | feature_flags: FeatureFlags.all
65 | })
66 |
67 | serializer.serialize(User.first)
68 |
69 | ```
70 |
71 | ## Filters
72 |
73 | Filters allows us to reduce the amount of attributes we can serialize, therefore reduce the data usage & performance of serializing.
74 |
75 | There are two types of filters:
76 |
77 | - only - use those attributes **only** and nothing else.
78 | - except - all attributes **except** those attributes.
79 |
80 | Usage example:
81 |
82 | ```ruby
83 |
84 | class UserSerializer < Panko::Serializer
85 | attributes :id, :name, :email
86 | end
87 |
88 | # this line will return { 'name': '..' }
89 | UserSerializer.new(only: [:name]).serialize(User.first)
90 |
91 | # this line will return { 'id': '..', 'email': ... }
92 | UserSerializer.new(except: [:name]).serialize(User.first)
93 |
94 | ```
95 |
96 | > **Note** that if you want to user filter on an associations, the `:name` property is not taken into account.
97 | If you have a `has_many :state_transitions, name: :history` association defined, the key to use in filters is
98 | `:state_transitions` (e.g. `{ except: [:state_transitions] }`).
99 |
100 | ## Filters For
101 |
102 | Sometimes you find yourself having the same filtering logic in actions. In order to
103 | solve this duplication, Panko allows you to write the filters in the serializer.
104 |
105 | ```ruby
106 |
107 | class UserSerializer < Panko::Serializer
108 | attributes :id, :name, :email
109 |
110 | def self.filters_for(context, scope)
111 | {
112 | only: [:name]
113 | }
114 | end
115 | end
116 |
117 | # this line will return { 'name': '..' }
118 | UserSerializer.serialize(User.first)
119 |
120 | ```
121 |
122 | > See discussion in: [https:](https://github.com/yosiat/panko_serializer/issues/16)
123 |
124 | ## Aliases
125 |
126 | Let's say we have an attribute name that we want to expose to client as different name, the current way of doing so is using method attribute, for example:
127 |
128 | ```ruby
129 |
130 | class PostSerializer < Panko::Serializer
131 | attributes :published_at
132 |
133 | def published_at
134 | object.created_at
135 | end
136 | end
137 |
138 | ```
139 |
140 | The downside of this approach is that `created_at` skips Panko's type casting, therefore we get a direct hit on performance.
141 |
142 | To fix this, we can use aliases:
143 |
144 | ```ruby
145 |
146 | class PostSerializer < Panko::Serializer
147 | aliases created_at: :published_at
148 | end
149 |
150 | ```
151 |
--------------------------------------------------------------------------------
/docs/docs/design-choices.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: design-choices
3 | title: Design Choices
4 | sidebar_label: Design Choices
5 | ---
6 | In short, Panko is a serializer for ActiveRecord objects (it can't serialize any other object), which strives for high performance & simple API (which is inspired by ActiveModelSerializers).
7 |
8 | Its performance is achieved by:
9 |
10 | - `Oj::StringWriter` - I will elaborate later.
11 | - Type casting — instead of relying on ActiveRecord to do its type cast, Panko is doing it by itself.
12 | - Figuring out the metadata, ahead of time — therefore, we ask less questions during the `serialization loop`.
13 |
14 | ## Serialization overview
15 |
16 | First, let's start with an overview. Let's say we want to serialize an `User` object, which has
17 | `first_name`, `last_name`, `age`, and `email` properties.
18 |
19 | The serializer definition will be something like this:
20 |
21 | ```ruby
22 |
23 | class UserSerializer < Panko::Serializer
24 | attributes :name, :age, :email
25 |
26 | def name
27 | "#{object.first_name} #{object.last_name}"
28 | end
29 | end
30 |
31 | ```
32 |
33 | And the usage of this serializer will be:
34 |
35 | ```ruby
36 |
37 | # fetch user from database
38 | user = User.first
39 |
40 | # create serializer, with empty options
41 | serializer = UserSerializer.new
42 |
43 | # serialize to JSON
44 | serializer.serialize_to_json(user)
45 |
46 | ```
47 |
48 | Let's go over the steps that Panko will execute behind the scenes for this flow.
49 | _I will skip the serializer definition part, because it's fairly simple and straightforward (see `lib/panko/serializer.rb`)._
50 |
51 | First step, while initializing the UserSerializer, we will create a **Serialization Descriptor** for this class.
52 | Serialization Descriptor's goal is to answer those questions:
53 |
54 | - Which fields do we have? In our case, `:age`, `:email`.
55 | - Which method fields do we have? In our case `:name`.
56 | - Which associations do we have (and their serialization descriptors)?
57 |
58 | The serialization description is also responsible for filtering the attributes (`only` \\ `except`).
59 |
60 | Now, that we have the serialization descriptor, we are finished with the Ruby part of Panko, and all we did here is done in _initialization time_ and now we move to C code.
61 |
62 | In C land, we take the `user` object and the serialization descriptor, and start the serialization process which is separated to 4 parts:
63 |
64 | - Serializing Fields - looping through serialization descriptor's `fields` and read them from the ActiveRecord object (see `Type Casting`) and write them to the writer.
65 | - Serializing Method Fields - creating (a cached) serializer instance, setting its `@object` and `@context`, calling all the method fields and writing them to the writer.
66 | - Serializing associations — this is simple, once we have fields + method fields, we just repeat the process.
67 |
68 | Once this is finished, we have a nice JSON string.
69 | Now let's dig deeper.
70 |
71 | ## Interesting parts
72 |
73 | ### Oj::StringWriter
74 |
75 | If you read the code of ActiveRecord serialization code in Ruby, you will observe this flow:
76 |
77 | 1. Get an array of ActiveRecord objects (`User.all` for example).
78 | 2. Build a new array of hashes where each hash is an `User` with the attributes we selected.
79 | 3. The JSON serializer, takes this array of hashes and loop them, and converts it to a JSON string.
80 |
81 | This entire process is expensive in terms of Memory & CPU, and this where the combination of Panko and Oj::StringWriter really shines.
82 |
83 | In Panko, the serialization process of the above is:
84 |
85 | 1. Get an array of ActiveRecord objects (`User.all` for example).
86 | 2. Create `Oj::StringWriter` and feed the values to it, via `push_value` / `push_object` / `push_object` and behind the scene, `Oj::StringWriter` will serialize the objects incrementally into a string.
87 | 3. Get from `Oj::StringWriter` the completed JSON string — which is a no-op, since `Oj::StringWriter` already built the string.
88 |
89 | ### Figuring out the metadata, ahead of time.
90 |
91 | Another observation I noticed in the Ruby serializers is that they ask and do a lot in a serialization loop:
92 |
93 | - Is this field a method? is it a property?
94 | - Which fields and associations do I need for the serializer to consider the `only` and `except` options?
95 | - What is the serializer of this has_one association?
96 |
97 | Panko tries to ask the bare minimum in serialization by building `Serialization Descriptor` for each serialization and caching it.
98 |
99 | The Serialization Descriptor will do the filtering of `only` and `except` and will check if a field is a method or not (therefore Panko doesn't have list of `attributes`).
100 |
101 | ### Type Casting
102 |
103 | This is the final part, which helped yield most of the performance improvements.
104 | In ActiveRecord, when we read the value of an attribute, it does type casting of the DB value to its real Ruby type.
105 |
106 | For example, time strings are converted to Time objects, Strings are duplicated, and Integers are converted from their values to Number.
107 |
108 | This type casting is really expensive, as it's responsible for most of the allocations in the serialization flow and most of them can be "relaxed".
109 |
110 | If we think about it, we don't need to duplicate strings or convert time strings to time objects or even parse JSON strings for the JSON serialization process.
111 |
112 | What Panko does is that if we have ActiveRecord type string, we won't duplicate it.
113 | If we have an integer string value, we will convert it to an integer, and the same goes for other types.
114 |
115 | All of these conversions are done in C, which of course yields a big performance improvement.
116 |
117 | #### Time type casting
118 |
119 | While you read Panko source code, you will encounter the time type casting and immediately you will have a "WTF?" moment.
120 |
121 | The idea behind the time type casting code relies on the end result of JSON type casting — what we need in order to serialize Time to JSON? UTC ISO8601 time format representation.
122 |
123 | The time type casting works as follows:
124 |
125 | - If it's a string that ends with `Z`, and the strings matches the UTC ISO8601 regex, then we just return the string.
126 | - If it's a string and it doesn't follow the rules above, we check if it's a timestamp in database format and convert it via regex + string concat to UTC ISO8601 - Yes, there is huge assumption here, that the database returns UTC timestamps — this will be configurable (before Panko official release).
127 | - If it's none of the above, I will let ActiveRecord type casting do it's magic.
128 |
--------------------------------------------------------------------------------
/docs/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: getting-started
3 | title: Getting Started
4 | sidebar_label: Getting Started
5 | ---
6 | ## Installation
7 |
8 | To install Panko, all you need is to add it to your Gemfile:
9 |
10 | ```ruby
11 |
12 | gem "panko_serializer"
13 |
14 | ```
15 |
16 | Then, install it on the command line:
17 |
18 | ```
19 |
20 | bundle install
21 |
22 | ```
23 |
24 | ## Creating your first serializer
25 |
26 | Let's create a serializer and use it inside of a Rails controller:
27 |
28 | ```ruby
29 | class PostSerializer < Panko::Serializer
30 | attributes :title
31 | end
32 |
33 | class UserSerializer < Panko::Serializer
34 | attributes :id, :name, :age
35 |
36 | has_many :posts, serializer: PostSerializer
37 | end
38 | ```
39 |
40 | ### Serializing an object
41 |
42 | And now serialize a single object:
43 |
44 | ```ruby
45 |
46 | # Using Oj serializer
47 | PostSerializer.new.serialize_to_json(Post.first)
48 |
49 | # or, similar to #serializable_hash
50 | PostSerializer.new.serialize(Post.first).to_json
51 |
52 | ```
53 |
54 | ### Using the serializers in a controller
55 |
56 | As you can see, defining serializers is simple and resembles ActiveModelSerializers 0.9.
57 | To utilize the `UserSerializer` inside a Rails controller and serialize some users, all we need to do is:
58 |
59 | ```ruby
60 |
61 | class UsersController < ApplicationController
62 | def index
63 | users = User.includes(:posts).all
64 | render json: Panko::ArraySerializer.new(users, each_serializer: UserSerializer).to_json
65 | end
66 | end
67 |
68 | ```
69 |
70 | And voila, we have an endpoint which serializes users using Panko!
71 |
--------------------------------------------------------------------------------
/docs/docs/introduction.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: index
3 | title: Introduction
4 | sidebar_label: Introduction
5 | slug: /
6 | ---
7 | Panko is a library which is inspired by ActiveModelSerializers 0.9 for serializing ActiveRecord/Ruby objects to JSON strings, fast.
8 |
9 | To achieve it's [performance](https://panko.dev/docs/performance/):
10 |
11 | - Oj - Panko relies on Oj since it's fast and allow to serialize incrementally using `Oj::StringWriter`.
12 | - Serialization Descriptor - Panko computes most of the metadata ahead of time, to save time later in serialization.
13 | - Type casting — Panko does type casting by itself, instead of relying on ActiveRecord.
14 |
--------------------------------------------------------------------------------
/docs/docs/performance.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: performance
3 | title: Performance
4 | sidebar_label: Performance
5 | ---
6 | The performance of Panko is measured using microbenchmarks and load testing.
7 |
8 | ## Microbenchmarks
9 |
10 | The following microbenchmarks are run on MacBook Pro (16-inch, 2021, M1 Max), Ruby 3.2.0 with Rails 7.0.5
11 | demonstrating the performance of ActiveModelSerializers 0.10.13 and Panko 0.8.0.
12 |
13 | | Benchmark | AMS ip/s | Panko ip/s |
14 | | ----------------- | -------- | ---------- |
15 | | Simple_Posts_2300 | 11.72 | 523.05 |
16 | | Simple_Posts_50 | 557.29 | 23,011.9 |
17 | | HasOne_Posts_2300 | 5.91 | 233.44 |
18 | | HasOne_Posts_50 | 285.8 | 10,362.79 |
19 |
20 | ## Real-world benchmark
21 |
22 | The real-world benchmark here is an endpoint which serializes 7,884 entries with 48 attributes and no associations.
23 | The benchmark took place in an environment that simulates production environment and run using `wrk` from machine on the same cluster.
24 |
25 | | Metric | AMS | Panko |
26 | | ------------------ | ----- | ----- |
27 | | Avg Response Time | 4.89s | 1.48s |
28 | | Max Response Time | 5.42s | 1.83s |
29 | | 99th Response Time | 5.42s | 1.74s |
30 | | Total Requests | 61 | 202 |
31 |
32 | _Thanks to [Bringg](https://www.bringg.com) for providing the infrastructure for the benchmarks._
33 |
--------------------------------------------------------------------------------
/docs/docs/response-bag.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: response-bag
3 | title: Response
4 | sidebar_label: Response
5 | ---
6 | Let's say you have some JSON payload which is constructed using Panko serialization result,
7 | like this:
8 |
9 | ```ruby
10 |
11 | class PostsController < ApplicationController
12 | def index
13 | posts = Post.all
14 | render json: {
15 | success: true,
16 | total_count: posts.count,
17 | posts: Panko::ArraySerializer.new(posts, each_serializer: PostSerializer).to_json
18 | }
19 | end
20 | end
21 |
22 | ```
23 |
24 | The output of the above will be a JSON string (for `posts`) inside a JSON string and this were `Panko::Response` shines.
25 |
26 | ```ruby
27 |
28 | class PostsController < ApplicationController
29 | def index
30 | posts = Post.all
31 | render json: Panko::Response.new(
32 | success: true,
33 | total_count: posts.count,
34 | posts: Panko::ArraySerializer.new(posts, each_serializer: PostSerializer)
35 | )
36 | end
37 | end
38 |
39 | ```
40 |
41 | And everything will work as expected!
42 |
43 | For a single object serialization, we need to use a different API (since `Panko::Serializer` doesn't accept an object in it's constructor):
44 |
45 | ```ruby
46 |
47 | class PostsController < ApplicationController
48 | def show
49 | post = Post.find(params[:id])
50 |
51 | render(
52 | json: Panko::Response.create do |r|
53 | {
54 | success: true,
55 | post: r.serializer(post, PostSerializer)
56 | }
57 | end
58 | )
59 | end
60 | end
61 |
62 | ```
63 |
64 | ## JsonValue
65 |
66 | Let's take the above example further, we will serialize the posts and cache it as JSON string in our Cache.
67 | Now, you can wrap the cached value with `Panko::JsonValue`, like here:
68 |
69 | ```ruby
70 |
71 | class PostsController < ApplicationController
72 | def index
73 | posts = Cache.get("/posts")
74 |
75 | render json: Panko::Response.new(
76 | success: true,
77 | total_count: posts.count,
78 | posts: Panko::JsonValue.from(posts)
79 | )
80 | end
81 | end
82 |
83 | ```
84 |
--------------------------------------------------------------------------------
/docs/docusaurus.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "title": "Panko Serializers",
3 | "tagline": "High Performance JSON Serialization for ActiveRecord & Ruby Objects",
4 | "url": "https://panko.dev",
5 | "baseUrl": "/",
6 | "organizationName": "yosiat",
7 | "projectName": "panko_serializer",
8 | "favicon": "favicon.ico",
9 | "customFields": {
10 | "repoPath": "yosiat/panko_serializer",
11 | "repoUrl": "https://github.com/yosiat/panko_serializer",
12 | },
13 | "onBrokenLinks": "log",
14 | "onBrokenMarkdownLinks": "log",
15 | "presets": [
16 | [
17 | "@docusaurus/preset-classic",
18 | {
19 | "docs": {
20 | "path": "./docs",
21 | "showLastUpdateAuthor": false,
22 | "showLastUpdateTime": false,
23 | "sidebarPath": "./sidebars.json",
24 | "routeBasePath": process.env.NODE_ENV === 'production' ? '/docs' : '/',
25 | },
26 | "blog": false,
27 | "pages": false,
28 | "theme": {
29 | "customCss": "./src/css/customTheme.css"
30 | }
31 | }
32 | ]
33 | ],
34 | "plugins": [],
35 | "themeConfig": {
36 | colorMode: {
37 | defaultMode: 'light',
38 | disableSwitch: true,
39 | },
40 | "navbar": {
41 | "title": "Panko Serializers",
42 | "items": [
43 | {
44 | "to": "introduction",
45 | "label": "Docs",
46 | "position": "left"
47 | }
48 | ]
49 | },
50 | "image": "img/undraw_online.svg",
51 | "footer": {
52 | "links": [
53 | {
54 | title: "GitHub",
55 | items: [
56 | {
57 | label: "Repository",
58 | href: "https://github.com/yosiat/panko_serializer"
59 | },
60 | {
61 | label: "Discussions",
62 | href: "https://github.com/yosiat/panko_serializer/discussions"
63 | },
64 | {
65 | label: "Issues",
66 | href: "https://github.com/yosiat/panko_serializer/issues"
67 | },
68 | {
69 | "html": `
70 | `
74 | }
75 | ]
76 | }
77 | ],
78 | "copyright": `Copyright © ${new Date().getFullYear()} Panko Serializer`,
79 | },
80 | prism: {
81 | theme: require('prism-react-renderer').themes.github, // Optional: Customize theme
82 | darkTheme: require('prism-react-renderer').themes.dracula, // Optional: Dark theme
83 | additionalLanguages: ['ruby'], // Add Ruby as an additional language
84 | },
85 | }
86 | }
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "examples": "docusaurus-examples",
4 | "start": "docusaurus start",
5 | "build": "docusaurus build",
6 | "serve": "docusaurus serve",
7 | "write-translations": "docusaurus-write-translations",
8 | "version": "docusaurus-version",
9 | "rename-version": "docusaurus-rename-version",
10 | "swizzle": "docusaurus swizzle",
11 | "deploy": "docusaurus deploy",
12 | "docusaurus": "docusaurus"
13 | },
14 | "dependencies": {
15 | "@docusaurus/core": "^3.7.0",
16 | "@docusaurus/preset-classic": "^3.7.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/docs/sidebars.json:
--------------------------------------------------------------------------------
1 | {
2 | "docs": {
3 | "Panko": [
4 | "index",
5 | "getting-started",
6 | "performance",
7 | "design-choices"
8 | ],
9 | "Reference": [
10 | "attributes",
11 | "associations",
12 | "response-bag"
13 | ]
14 | }
15 | }
--------------------------------------------------------------------------------
/docs/src/css/customTheme.css:
--------------------------------------------------------------------------------
1 | :root{
2 | --ifm-color-primary-lightest: #D34A4B;
3 | --ifm-color-primary-lighter: #CA3133;
4 | --ifm-color-primary-light: #C22F30;
5 | --ifm-color-primary: #B02B2C;
6 | --ifm-color-primary-dark: #9E2728;
7 | --ifm-color-primary-darker: #962525;
8 | --ifm-color-primary-darkest: #7B1E1F;
9 | }
10 |
--------------------------------------------------------------------------------
/docs/static/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yosiat/panko_serializer/38967a598163efb71b2ac6edbf168df396aafda3/docs/static/.DS_Store
--------------------------------------------------------------------------------
/docs/static/CNAME:
--------------------------------------------------------------------------------
1 | panko.dev
2 |
--------------------------------------------------------------------------------
/docs/static/css/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | /* your custom css */
9 |
10 | @media only screen and (min-device-width: 360px) and (max-device-width: 736px) {
11 | }
12 |
13 | @media only screen and (min-width: 1024px) {
14 | }
15 |
16 | @media only screen and (max-width: 1023px) {
17 | }
18 |
19 | @media only screen and (min-width: 1400px) {
20 | }
21 |
22 | @media only screen and (min-width: 1500px) {
23 | }
24 |
25 | .homeSplashFade {
26 | max-width: 850px;
27 | margin-left: auto;
28 | margin-right: auto;
29 | }
30 |
31 | body {
32 | color: #24292e;
33 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
34 | font-size: 16px;
35 | line-height: 1.5;
36 | -ms-text-size-adjust: 100%;
37 | -webkit-text-size-adjust: 100%;
38 | word-wrap: break-word;
39 | }
40 |
41 | h2 {
42 | font-weight: 200;
43 | }
44 |
45 | .homeContainer {
46 | background-color: #3F4C6B;
47 | padding: 32px;
48 | text-align: center;
49 | }
50 |
51 | .projectTitle { color: white; }
--------------------------------------------------------------------------------
/docs/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yosiat/panko_serializer/38967a598163efb71b2ac6edbf168df396aafda3/docs/static/img/favicon.ico
--------------------------------------------------------------------------------
/docs/static/img/oss_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yosiat/panko_serializer/38967a598163efb71b2ac6edbf168df396aafda3/docs/static/img/oss_logo.png
--------------------------------------------------------------------------------
/docs/static/img/undraw_tweetstorm.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 | panko-serializer
10 |
11 |
12 | If you are not redirected automatically, follow this link.
13 |
14 |
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/active_record.c:
--------------------------------------------------------------------------------
1 | #include "active_record.h"
2 |
3 | static ID attributes_id;
4 | static ID types_id;
5 | static ID additional_types_id;
6 | static ID values_id;
7 | static ID delegate_hash_id;
8 |
9 | static ID value_before_type_cast_id;
10 | static ID type_id;
11 |
12 | static ID fetch_id;
13 |
14 | // ActiveRecord::Result::IndexedRow
15 | static VALUE ar_result_indexed_row = Qundef;
16 | static int fetched_ar_result_indexed_row = 0;
17 |
18 | VALUE fetch_ar_result_indexed_row_type() {
19 | if (fetched_ar_result_indexed_row == 1) {
20 | return ar_result_indexed_row;
21 | }
22 |
23 | fetched_ar_result_indexed_row = 1;
24 |
25 | VALUE ar, ar_result;
26 |
27 | ar = rb_const_get_at(rb_cObject, rb_intern("ActiveRecord"));
28 |
29 | // ActiveRecord::Result
30 | ar_result = rb_const_get_at(ar, rb_intern("Result"));
31 |
32 | if (rb_const_defined_at(ar_result, rb_intern("IndexedRow")) == (int)Qtrue) {
33 | ar_result_indexed_row = rb_const_get_at(ar_result, rb_intern("IndexedRow"));
34 | }
35 |
36 | return ar_result_indexed_row;
37 | }
38 |
39 | struct attributes {
40 | // Hash
41 | VALUE attributes_hash;
42 | size_t attributes_hash_size;
43 |
44 | // Hash
45 | VALUE types;
46 | // Hash
47 | VALUE additional_types;
48 | // heuristics
49 | bool tryToReadFromAdditionalTypes;
50 |
51 | // Rails <8: Hash
52 | // Rails >=8: ActiveRecord::Result::IndexedRow
53 | VALUE values;
54 |
55 | // Hash
56 | VALUE indexed_row_column_indexes;
57 | // Array or NIL
58 | VALUE indexed_row_row;
59 | bool is_indexed_row;
60 | };
61 |
62 | struct attributes init_context(VALUE obj) {
63 | volatile VALUE attributes_set = rb_ivar_get(obj, attributes_id);
64 | volatile VALUE attributes_hash = rb_ivar_get(attributes_set, attributes_id);
65 |
66 | struct attributes attrs = (struct attributes){
67 | .attributes_hash =
68 | PANKO_EMPTY_HASH(attributes_hash) ? Qnil : attributes_hash,
69 | .attributes_hash_size = 0,
70 |
71 | .types = rb_ivar_get(attributes_set, types_id),
72 | .additional_types = rb_ivar_get(attributes_set, additional_types_id),
73 | .tryToReadFromAdditionalTypes =
74 | PANKO_EMPTY_HASH(rb_ivar_get(attributes_set, additional_types_id)) ==
75 | false,
76 |
77 | .values = rb_ivar_get(attributes_set, values_id),
78 | .is_indexed_row = false,
79 | .indexed_row_column_indexes = Qnil,
80 | .indexed_row_row = Qnil,
81 | };
82 |
83 | if (attrs.attributes_hash != Qnil) {
84 | attrs.attributes_hash_size = RHASH_SIZE(attrs.attributes_hash);
85 | }
86 |
87 | if (CLASS_OF(attrs.values) == fetch_ar_result_indexed_row_type()) {
88 | volatile VALUE indexed_row_column_indexes =
89 | rb_ivar_get(attrs.values, rb_intern("@column_indexes"));
90 |
91 | volatile VALUE indexed_row_row =
92 | rb_ivar_get(attrs.values, rb_intern("@row"));
93 |
94 | attrs.indexed_row_column_indexes = indexed_row_column_indexes;
95 | attrs.indexed_row_row = indexed_row_row;
96 | attrs.is_indexed_row = true;
97 | }
98 |
99 | return attrs;
100 | }
101 |
102 | VALUE _read_value_from_indexed_row(struct attributes attributes_ctx,
103 | volatile VALUE member) {
104 | volatile VALUE value = Qnil;
105 |
106 | if (NIL_P(attributes_ctx.indexed_row_column_indexes) ||
107 | NIL_P(attributes_ctx.indexed_row_row)) {
108 | return value;
109 | }
110 |
111 | volatile VALUE column_index =
112 | rb_hash_aref(attributes_ctx.indexed_row_column_indexes, member);
113 |
114 | if (NIL_P(column_index)) {
115 | return value;
116 | }
117 |
118 | volatile VALUE row = attributes_ctx.indexed_row_row;
119 | if (NIL_P(row)) {
120 | return value;
121 | }
122 |
123 | return RARRAY_AREF(row, NUM2INT(column_index));
124 | }
125 |
126 | VALUE read_attribute(struct attributes attributes_ctx, Attribute attribute,
127 | volatile VALUE* isJson) {
128 | volatile VALUE member, value;
129 |
130 | member = attribute->name_str;
131 | value = Qnil;
132 |
133 | if (
134 | // we have attributes_hash
135 | !NIL_P(attributes_ctx.attributes_hash)
136 | // It's not empty
137 | && (attributes_ctx.attributes_hash_size > 0)) {
138 | volatile VALUE attribute_metadata =
139 | rb_hash_aref(attributes_ctx.attributes_hash, member);
140 |
141 | if (attribute_metadata != Qnil) {
142 | value = rb_ivar_get(attribute_metadata, value_before_type_cast_id);
143 |
144 | if (NIL_P(attribute->type)) {
145 | attribute->type = rb_ivar_get(attribute_metadata, type_id);
146 | }
147 | }
148 | }
149 |
150 | if (NIL_P(value) && !NIL_P(attributes_ctx.values)) {
151 | if (attributes_ctx.is_indexed_row == true) {
152 | value = _read_value_from_indexed_row(attributes_ctx, member);
153 | } else {
154 | value = rb_hash_aref(attributes_ctx.values, member);
155 | }
156 | }
157 |
158 | if (NIL_P(attribute->type) && !NIL_P(value)) {
159 | if (attributes_ctx.tryToReadFromAdditionalTypes == true) {
160 | attribute->type = rb_hash_aref(attributes_ctx.additional_types, member);
161 | }
162 |
163 | if (!NIL_P(attributes_ctx.types) && NIL_P(attribute->type)) {
164 | attribute->type = rb_hash_aref(attributes_ctx.types, member);
165 | }
166 | }
167 |
168 | if (!NIL_P(attribute->type) && !NIL_P(value)) {
169 | return type_cast(attribute->type, value, isJson);
170 | }
171 |
172 | return value;
173 | }
174 |
175 | void active_record_attributes_writer(VALUE obj, VALUE attributes,
176 | EachAttributeFunc write_value,
177 | VALUE writer) {
178 | long i;
179 | struct attributes attributes_ctx = init_context(obj);
180 | volatile VALUE record_class = CLASS_OF(obj);
181 |
182 | for (i = 0; i < RARRAY_LEN(attributes); i++) {
183 | volatile VALUE raw_attribute = RARRAY_AREF(attributes, i);
184 | Attribute attribute = PANKO_ATTRIBUTE_READ(raw_attribute);
185 | attribute_try_invalidate(attribute, record_class);
186 |
187 | volatile VALUE isJson = Qfalse;
188 | volatile VALUE value = read_attribute(attributes_ctx, attribute, &isJson);
189 |
190 | write_value(writer, attr_name_for_serialization(attribute), value, isJson);
191 | }
192 | }
193 |
194 | void init_active_record_attributes_writer(VALUE mPanko) {
195 | attributes_id = rb_intern("@attributes");
196 | delegate_hash_id = rb_intern("@delegate_hash");
197 | values_id = rb_intern("@values");
198 | types_id = rb_intern("@types");
199 | additional_types_id = rb_intern("@additional_types");
200 | type_id = rb_intern("@type");
201 | value_before_type_cast_id = rb_intern("@value_before_type_cast");
202 | fetch_id = rb_intern("fetch");
203 | }
204 |
205 | void panko_init_active_record(VALUE mPanko) {
206 | init_active_record_attributes_writer(mPanko);
207 | panko_init_type_cast(mPanko);
208 | }
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/active_record.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | #include "../common.h"
7 | #include "common.h"
8 | #include "serialization_descriptor/attribute.h"
9 | #include "type_cast/type_cast.h"
10 |
11 | extern void active_record_attributes_writer(VALUE object, VALUE attributes,
12 | EachAttributeFunc func,
13 | VALUE writer);
14 |
15 | void init_active_record_attributes_writer(VALUE mPanko);
16 |
17 | void panko_init_active_record(VALUE mPanko);
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/attributes_writer.c:
--------------------------------------------------------------------------------
1 | #include "attributes_writer.h"
2 |
3 | static bool types_initialized = false;
4 | static VALUE ar_base_type = Qundef;
5 |
6 | VALUE init_types(VALUE v) {
7 | if (types_initialized == true) {
8 | return Qundef;
9 | }
10 |
11 | types_initialized = true;
12 |
13 | volatile VALUE ar_type =
14 | rb_const_get_at(rb_cObject, rb_intern("ActiveRecord"));
15 |
16 | ar_base_type = rb_const_get_at(ar_type, rb_intern("Base"));
17 | rb_global_variable(&ar_base_type);
18 |
19 | return Qundef;
20 | }
21 |
22 | AttributesWriter create_attributes_writer(VALUE object) {
23 | // If ActiveRecord::Base can't be found it will throw error
24 | int isErrored;
25 | rb_protect(init_types, Qnil, &isErrored);
26 |
27 | if (ar_base_type != Qundef &&
28 | rb_obj_is_kind_of(object, ar_base_type) == Qtrue) {
29 | return (AttributesWriter){
30 | .object_type = ActiveRecord,
31 | .write_attributes = active_record_attributes_writer};
32 | }
33 |
34 | if (!RB_SPECIAL_CONST_P(object) && BUILTIN_TYPE(object) == T_HASH) {
35 | return (AttributesWriter){.object_type = Hash,
36 | .write_attributes = hash_attributes_writer};
37 | }
38 |
39 | return (AttributesWriter){.object_type = Plain,
40 | .write_attributes = plain_attributes_writer};
41 |
42 | return create_empty_attributes_writer();
43 | }
44 |
45 | void empty_write_attributes(VALUE obj, VALUE attributes, EachAttributeFunc func,
46 | VALUE writer) {}
47 |
48 | AttributesWriter create_empty_attributes_writer() {
49 | return (AttributesWriter){.object_type = UnknownObjectType,
50 | .write_attributes = empty_write_attributes};
51 | }
52 |
53 | void init_attributes_writer(VALUE mPanko) {
54 | init_active_record_attributes_writer(mPanko);
55 | }
56 |
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/attributes_writer.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include "active_record.h"
6 | #include "common.h"
7 | #include "hash.h"
8 | #include "plain.h"
9 |
10 | enum ObjectType {
11 | UnknownObjectType = 0,
12 | ActiveRecord = 1,
13 | Plain = 2,
14 | Hash = 3
15 | };
16 |
17 | typedef struct _AttributesWriter {
18 | enum ObjectType object_type;
19 |
20 | void (*write_attributes)(VALUE object, VALUE attributes,
21 | EachAttributeFunc func, VALUE context);
22 | } AttributesWriter;
23 |
24 | /**
25 | * Infers the attributes writer from the object type
26 | */
27 | AttributesWriter create_attributes_writer(VALUE object);
28 |
29 | /**
30 | * Creates empty writer
31 | * Useful when the writer is not known, and you need init something
32 | */
33 | AttributesWriter create_empty_attributes_writer();
34 |
35 | void init_attributes_writer(VALUE mPanko);
36 |
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/common.c:
--------------------------------------------------------------------------------
1 | #include "common.h"
2 |
3 | VALUE attr_name_for_serialization(Attribute attribute) {
4 | volatile VALUE name_str = attribute->name_str;
5 | if (attribute->alias_name != Qnil) {
6 | name_str = attribute->alias_name;
7 | }
8 |
9 | return name_str;
10 | }
11 |
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/common.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "../serialization_descriptor/attribute.h"
4 | #include "ruby.h"
5 |
6 | typedef void (*EachAttributeFunc)(VALUE writer, VALUE name, VALUE value,
7 | VALUE isJson);
8 |
9 | VALUE attr_name_for_serialization(Attribute attribute);
10 |
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/hash.c:
--------------------------------------------------------------------------------
1 | #include "hash.h"
2 |
3 | void hash_attributes_writer(VALUE obj, VALUE attributes,
4 | EachAttributeFunc write_value, VALUE writer) {
5 | long i;
6 | for (i = 0; i < RARRAY_LEN(attributes); i++) {
7 | volatile VALUE raw_attribute = RARRAY_AREF(attributes, i);
8 | Attribute attribute = attribute_read(raw_attribute);
9 |
10 | write_value(writer, attr_name_for_serialization(attribute),
11 | rb_hash_aref(obj, attribute->name_str), Qfalse);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/hash.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "common.h"
4 | #include "ruby.h"
5 |
6 | void hash_attributes_writer(VALUE obj, VALUE attributes, EachAttributeFunc func,
7 | VALUE writer);
8 |
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/plain.c:
--------------------------------------------------------------------------------
1 | #include "plain.h"
2 |
3 | void plain_attributes_writer(VALUE obj, VALUE attributes,
4 | EachAttributeFunc write_value, VALUE writer) {
5 | long i;
6 | for (i = 0; i < RARRAY_LEN(attributes); i++) {
7 | volatile VALUE raw_attribute = RARRAY_AREF(attributes, i);
8 | Attribute attribute = attribute_read(raw_attribute);
9 |
10 | write_value(writer, attr_name_for_serialization(attribute),
11 | rb_funcall(obj, attribute->name_id, 0), Qfalse);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/plain.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "common.h"
4 | #include "ruby.h"
5 |
6 | void plain_attributes_writer(VALUE obj, VALUE attributes,
7 | EachAttributeFunc func, VALUE writer);
8 |
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/type_cast/time_conversion.c:
--------------------------------------------------------------------------------
1 | #include "time_conversion.h"
2 |
3 | const int YEAR_REGION = 1;
4 | const int MONTH_REGION = 2;
5 | const int DAY_REGION = 3;
6 | const int HOUR_REGION = 4;
7 | const int MINUTE_REGION = 5;
8 | const int SECOND_REGION = 6;
9 |
10 | static regex_t* iso8601_time_regex;
11 | static regex_t* ar_iso_datetime_regex;
12 |
13 | VALUE is_iso8601_time_string(const char* value) {
14 | const UChar *start, *range, *end;
15 | OnigPosition r;
16 |
17 | const UChar* str = (const UChar*)(value);
18 |
19 | end = str + strlen(value);
20 | start = str;
21 | range = end;
22 | r = onig_search(iso8601_time_regex, str, end, start, range, NULL,
23 | ONIG_OPTION_NONE);
24 |
25 | return r >= 0 ? Qtrue : Qfalse;
26 | }
27 |
28 | void append_region_str(const char* source, char** to, int regionBegin,
29 | int regionEnd) {
30 | long iter = 0;
31 | for (iter = regionBegin; iter < regionEnd; iter++) {
32 | *(*to)++ = source[iter];
33 | }
34 | }
35 |
36 | bool is_iso_ar_iso_datetime_string_fast_case(const char* value) {
37 | return (
38 | // year
39 | isdigit(value[0]) && isdigit(value[1]) && isdigit(value[2]) &&
40 | isdigit(value[3]) && value[4] == '-' &&
41 | // month
42 | isdigit(value[5]) && isdigit(value[6]) && value[7] == '-' &&
43 | // mday
44 | isdigit(value[8]) && isdigit(value[9]) && value[10] == ' ' &&
45 |
46 | // hour
47 | isdigit(value[11]) && isdigit(value[12]) && value[13] == ':' &&
48 | // minute
49 | isdigit(value[14]) && isdigit(value[15]) && value[16] == ':' &&
50 | // seconds
51 | isdigit(value[17]) && isdigit(value[18]));
52 | }
53 |
54 | bool is_iso_ar_iso_datetime_string_slow_case(const char* value) {
55 | const UChar *start, *range, *end;
56 | OnigPosition r;
57 | OnigRegion* region = onig_region_new();
58 |
59 | const UChar* str = (const UChar*)(value);
60 |
61 | end = str + strlen(value);
62 | start = str;
63 | range = end;
64 | r = onig_search(ar_iso_datetime_regex, str, end, start, range, region,
65 | ONIG_OPTION_NONE);
66 |
67 | onig_region_free(region, 1);
68 |
69 | return (r >= 0);
70 | }
71 |
72 | VALUE iso_ar_iso_datetime_string(const char* value) {
73 | if (is_iso_ar_iso_datetime_string_fast_case(value) == true ||
74 | is_iso_ar_iso_datetime_string_slow_case(value) == true) {
75 | volatile VALUE output;
76 |
77 | char buf[24] = "";
78 | char* cur = buf;
79 |
80 | append_region_str(value, &cur, 0, 4);
81 | *cur++ = '-';
82 |
83 | append_region_str(value, &cur, 5, 7);
84 | *cur++ = '-';
85 |
86 | append_region_str(value, &cur, 8, 10);
87 | *cur++ = 'T';
88 |
89 | append_region_str(value, &cur, 11, 13);
90 | *cur++ = ':';
91 |
92 | append_region_str(value, &cur, 14, 16);
93 | *cur++ = ':';
94 |
95 | append_region_str(value, &cur, 17, 19);
96 |
97 | *cur++ = '.';
98 | if (value[19] == '.' && isdigit(value[20])) {
99 | if (isdigit(value[20])) {
100 | *cur++ = value[20];
101 | } else {
102 | *cur++ = '0';
103 | }
104 |
105 | if (isdigit(value[21])) {
106 | *cur++ = value[21];
107 | } else {
108 | *cur++ = '0';
109 | }
110 |
111 | if (isdigit(value[22])) {
112 | *cur++ = value[22];
113 | } else {
114 | *cur++ = '0';
115 | }
116 | } else {
117 | *cur++ = '0';
118 | *cur++ = '0';
119 | *cur++ = '0';
120 | }
121 | *cur++ = 'Z';
122 |
123 | output = rb_str_new(buf, cur - buf);
124 | return output;
125 | }
126 |
127 | return Qnil;
128 | }
129 |
130 | void build_regex(OnigRegex* reg, const UChar* pattern) {
131 | OnigErrorInfo einfo;
132 |
133 | int r = onig_new(reg, pattern, pattern + strlen((char*)pattern),
134 | ONIG_OPTION_DEFAULT, ONIG_ENCODING_ASCII,
135 | ONIG_SYNTAX_DEFAULT, &einfo);
136 |
137 | if (r != ONIG_NORMAL) {
138 | char s[ONIG_MAX_ERROR_MESSAGE_LEN];
139 | onig_error_code_to_str((UChar*)s, r, &einfo);
140 | printf("ERROR: %s\n", s);
141 | }
142 | }
143 |
144 | void panko_init_time(VALUE mPanko) {
145 | const UChar *ISO8601_PATTERN, *AR_ISO_DATETIME_PATTERN;
146 |
147 | ISO8601_PATTERN =
148 | (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)?)?)?)?$";
149 |
150 | build_regex(&iso8601_time_regex, ISO8601_PATTERN);
151 |
152 | AR_ISO_DATETIME_PATTERN =
153 | (UChar*)"\\A(?\\d{4})-(?\\d\\d)-(?\\d\\d) (?\\d\\d):(?\\d\\d):(?\\d\\d)(\\.(?\\d+))?\\z";
154 |
155 | build_regex(&ar_iso_datetime_regex, AR_ISO_DATETIME_PATTERN);
156 | }
157 |
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/type_cast/time_conversion.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | VALUE is_iso8601_time_string(const char* value);
9 | VALUE iso_ar_iso_datetime_string(const char* value);
10 | void panko_init_time(VALUE mPanko);
11 |
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/type_cast/type_cast.c:
--------------------------------------------------------------------------------
1 | #include "type_cast.h"
2 |
3 | #include "time_conversion.h"
4 |
5 | ID deserialize_from_db_id = 0;
6 | ID to_s_id = 0;
7 | ID to_i_id = 0;
8 |
9 | static VALUE oj_type = Qundef;
10 | static VALUE oj_parseerror_type = Qundef;
11 | ID oj_sc_parse_id = 0;
12 |
13 | // Caching ActiveRecord Types
14 | static VALUE ar_string_type = Qundef;
15 | static VALUE ar_text_type = Qundef;
16 | static VALUE ar_float_type = Qundef;
17 | static VALUE ar_integer_type = Qundef;
18 | static VALUE ar_boolean_type = Qundef;
19 | static VALUE ar_date_time_type = Qundef;
20 | static VALUE ar_time_zone_converter = Qundef;
21 | static VALUE ar_json_type = Qundef;
22 |
23 | static VALUE ar_pg_integer_type = Qundef;
24 | static VALUE ar_pg_float_type = Qundef;
25 | static VALUE ar_pg_uuid_type = Qundef;
26 | static VALUE ar_pg_json_type = Qundef;
27 | static VALUE ar_pg_jsonb_type = Qundef;
28 | static VALUE ar_pg_array_type = Qundef;
29 | static VALUE ar_pg_date_time_type = Qundef;
30 | static VALUE ar_pg_timestamp_type = Qundef;
31 |
32 | static int initiailized = 0;
33 |
34 | VALUE cache_postgres_type_lookup(VALUE ar) {
35 | VALUE ar_connection_adapters, ar_postgresql, ar_oid;
36 |
37 | ar_connection_adapters = rb_const_get_at(ar, rb_intern("ConnectionAdapters"));
38 | if (ar_connection_adapters == Qundef) {
39 | return Qfalse;
40 | }
41 |
42 | ar_postgresql =
43 | rb_const_get_at(ar_connection_adapters, rb_intern("PostgreSQL"));
44 | if (ar_postgresql == Qundef) {
45 | return Qfalse;
46 | }
47 |
48 | ar_oid = rb_const_get_at(ar_postgresql, rb_intern("OID"));
49 | if (ar_oid == Qundef) {
50 | return Qfalse;
51 | }
52 |
53 | if (rb_const_defined_at(ar_oid, rb_intern("Float")) == (int)Qtrue) {
54 | ar_pg_float_type = rb_const_get_at(ar_oid, rb_intern("Float"));
55 | }
56 |
57 | if (rb_const_defined_at(ar_oid, rb_intern("Integer")) == (int)Qtrue) {
58 | ar_pg_integer_type = rb_const_get_at(ar_oid, rb_intern("Integer"));
59 | }
60 |
61 | if (rb_const_defined_at(ar_oid, rb_intern("Uuid")) == (int)Qtrue) {
62 | ar_pg_uuid_type = rb_const_get_at(ar_oid, rb_intern("Uuid"));
63 | }
64 |
65 | if (rb_const_defined_at(ar_oid, rb_intern("Json")) == (int)Qtrue) {
66 | ar_pg_json_type = rb_const_get_at(ar_oid, rb_intern("Json"));
67 | }
68 |
69 | if (rb_const_defined_at(ar_oid, rb_intern("Jsonb")) == (int)Qtrue) {
70 | ar_pg_jsonb_type = rb_const_get_at(ar_oid, rb_intern("Jsonb"));
71 | }
72 |
73 | if (rb_const_defined_at(ar_oid, rb_intern("DateTime")) == (int)Qtrue) {
74 | ar_pg_date_time_type = rb_const_get_at(ar_oid, rb_intern("DateTime"));
75 | }
76 |
77 | if (rb_const_defined_at(ar_oid, rb_intern("Timestamp")) == (int)Qtrue) {
78 | ar_pg_timestamp_type = rb_const_get_at(ar_oid, rb_intern("Timestamp"));
79 | }
80 |
81 | return Qtrue;
82 | }
83 |
84 | VALUE cache_time_zone_type_lookup(VALUE ar) {
85 | VALUE ar_attr_methods, ar_time_zone_conversion;
86 |
87 | // ActiveRecord::AttributeMethods
88 | ar_attr_methods = rb_const_get_at(ar, rb_intern("AttributeMethods"));
89 | if (ar_attr_methods == Qundef) {
90 | return Qfalse;
91 | }
92 |
93 | // ActiveRecord::AttributeMethods::TimeZoneConversion
94 | ar_time_zone_conversion =
95 | rb_const_get_at(ar_attr_methods, rb_intern("TimeZoneConversion"));
96 | if (ar_time_zone_conversion == Qundef) {
97 | return Qfalse;
98 | }
99 |
100 | ar_time_zone_converter =
101 | rb_const_get_at(ar_time_zone_conversion, rb_intern("TimeZoneConverter"));
102 |
103 | return Qtrue;
104 | }
105 |
106 | void cache_type_lookup() {
107 | if (initiailized == 1) {
108 | return;
109 | }
110 |
111 | initiailized = 1;
112 |
113 | VALUE ar, ar_type, ar_type_methods;
114 |
115 | ar = rb_const_get_at(rb_cObject, rb_intern("ActiveRecord"));
116 |
117 | // ActiveRecord::Type
118 | ar_type = rb_const_get_at(ar, rb_intern("Type"));
119 |
120 | ar_string_type = rb_const_get_at(ar_type, rb_intern("String"));
121 | ar_text_type = rb_const_get_at(ar_type, rb_intern("Text"));
122 | ar_float_type = rb_const_get_at(ar_type, rb_intern("Float"));
123 | ar_integer_type = rb_const_get_at(ar_type, rb_intern("Integer"));
124 | ar_boolean_type = rb_const_get_at(ar_type, rb_intern("Boolean"));
125 | ar_date_time_type = rb_const_get_at(ar_type, rb_intern("DateTime"));
126 |
127 | ar_type_methods = rb_class_instance_methods(0, NULL, ar_string_type);
128 | if (rb_ary_includes(ar_type_methods,
129 | rb_to_symbol(rb_str_new_cstr("deserialize")))) {
130 | deserialize_from_db_id = rb_intern("deserialize");
131 | } else {
132 | deserialize_from_db_id = rb_intern("type_cast_from_database");
133 | }
134 |
135 | if (rb_const_defined_at(ar_type, rb_intern("Json")) == (int)Qtrue) {
136 | ar_json_type = rb_const_get_at(ar_type, rb_intern("Json"));
137 | }
138 |
139 | // TODO: if we get error or not, add this to some debug log
140 | int isErrored;
141 | rb_protect(cache_postgres_type_lookup, ar, &isErrored);
142 |
143 | rb_protect(cache_time_zone_type_lookup, ar, &isErrored);
144 | }
145 |
146 | bool is_string_or_text_type(VALUE type_klass) {
147 | return type_klass == ar_string_type || type_klass == ar_text_type ||
148 | (ar_pg_uuid_type != Qundef && type_klass == ar_pg_uuid_type);
149 | }
150 |
151 | VALUE cast_string_or_text_type(VALUE value) {
152 | if (RB_TYPE_P(value, T_STRING)) {
153 | return value;
154 | }
155 |
156 | if (value == Qtrue) {
157 | return rb_str_new_cstr("t");
158 | }
159 |
160 | if (value == Qfalse) {
161 | return rb_str_new_cstr("f");
162 | }
163 |
164 | return rb_funcall(value, to_s_id, 0);
165 | }
166 |
167 | bool is_float_type(VALUE type_klass) {
168 | return type_klass == ar_float_type ||
169 | (ar_pg_float_type != Qundef && type_klass == ar_pg_float_type);
170 | }
171 |
172 | VALUE cast_float_type(VALUE value) {
173 | if (RB_TYPE_P(value, T_FLOAT)) {
174 | return value;
175 | }
176 |
177 | if (RB_TYPE_P(value, T_STRING)) {
178 | const char* val = StringValuePtr(value);
179 | return rb_float_new(strtod(val, NULL));
180 | }
181 |
182 | return Qundef;
183 | }
184 |
185 | bool is_integer_type(VALUE type_klass) {
186 | return type_klass == ar_integer_type ||
187 | (ar_pg_integer_type != Qundef && type_klass == ar_pg_integer_type);
188 | }
189 |
190 | VALUE cast_integer_type(VALUE value) {
191 | if (RB_INTEGER_TYPE_P(value)) {
192 | return value;
193 | }
194 |
195 | if (RB_TYPE_P(value, T_STRING)) {
196 | const char* val = StringValuePtr(value);
197 | if (strlen(val) == 0) {
198 | return Qnil;
199 | }
200 | return rb_cstr2inum(val, 10);
201 | }
202 |
203 | if (RB_FLOAT_TYPE_P(value)) {
204 | // We are calling the `to_i` here, because ruby internal
205 | // `flo_to_i` is not accessible
206 | return rb_funcall(value, to_i_id, 0);
207 | }
208 |
209 | if (value == Qtrue) {
210 | return INT2NUM(1);
211 | }
212 |
213 | if (value == Qfalse) {
214 | return INT2NUM(0);
215 | }
216 |
217 | // At this point, we handled integer, float, string and booleans
218 | // any thing other than this (array, hashes, etc) should result in nil
219 | return Qnil;
220 | }
221 |
222 | bool is_json_type(VALUE type_klass) {
223 | return ((ar_pg_json_type != Qundef && type_klass == ar_pg_json_type) ||
224 | (ar_pg_jsonb_type != Qundef && type_klass == ar_pg_jsonb_type) ||
225 | (ar_json_type != Qundef && type_klass == ar_json_type));
226 | }
227 |
228 | bool is_boolean_type(VALUE type_klass) { return type_klass == ar_boolean_type; }
229 |
230 | VALUE cast_boolean_type(VALUE value) {
231 | if (value == Qtrue || value == Qfalse) {
232 | return value;
233 | }
234 |
235 | if (value == Qnil) {
236 | return Qnil;
237 | }
238 |
239 | if (RB_TYPE_P(value, T_STRING)) {
240 | if (RSTRING_LEN(value) == 0) {
241 | return Qnil;
242 | }
243 |
244 | const char* val = StringValuePtr(value);
245 |
246 | bool isFalseValue =
247 | (*val == '0' || (*val == 'f' || *val == 'F') ||
248 | (strcmp(val, "false") == 0 || strcmp(val, "FALSE") == 0) ||
249 | (strcmp(val, "off") == 0 || strcmp(val, "OFF") == 0));
250 |
251 | return isFalseValue ? Qfalse : Qtrue;
252 | }
253 |
254 | if (RB_INTEGER_TYPE_P(value)) {
255 | return value == INT2NUM(1) ? Qtrue : Qfalse;
256 | }
257 |
258 | return Qundef;
259 | }
260 |
261 | bool is_date_time_type(VALUE type_klass) {
262 | return (type_klass == ar_date_time_type) ||
263 | (ar_pg_date_time_type != Qundef &&
264 | type_klass == ar_pg_date_time_type) ||
265 | (ar_pg_timestamp_type != Qundef &&
266 | type_klass == ar_pg_timestamp_type) ||
267 | (ar_time_zone_converter != Qundef &&
268 | type_klass == ar_time_zone_converter);
269 | }
270 |
271 | VALUE cast_date_time_type(VALUE value) {
272 | // Instead of take strings to comparing them to time zones
273 | // and then comparing them back to string
274 | // We will just make sure we have string on ISO8601 and it's utc
275 | if (RB_TYPE_P(value, T_STRING)) {
276 | const char* val = StringValuePtr(value);
277 | // 'Z' in ISO8601 says it's UTC
278 | if (val[strlen(val) - 1] == 'Z' && is_iso8601_time_string(val) == Qtrue) {
279 | return value;
280 | }
281 |
282 | volatile VALUE iso8601_string = iso_ar_iso_datetime_string(val);
283 | if (iso8601_string != Qnil) {
284 | return iso8601_string;
285 | }
286 | }
287 |
288 | return Qundef;
289 | }
290 |
291 | VALUE rescue_func() { return Qfalse; }
292 |
293 | VALUE parse_json(VALUE value) {
294 | return rb_funcall(oj_type, oj_sc_parse_id, 2, rb_cObject, value);
295 | }
296 |
297 | VALUE is_json_value(VALUE value) {
298 | if (!RB_TYPE_P(value, T_STRING)) {
299 | return value;
300 | }
301 |
302 | if (RSTRING_LEN(value) == 0) {
303 | return Qfalse;
304 | }
305 |
306 | volatile VALUE result =
307 | rb_rescue2(parse_json, value, rescue_func, Qundef, oj_parseerror_type, 0);
308 |
309 | if (NIL_P(result)) {
310 | return Qtrue;
311 | }
312 |
313 | if (result == Qfalse) {
314 | return Qfalse;
315 | }
316 |
317 | // TODO: fix me!
318 | return Qfalse;
319 | }
320 |
321 | VALUE type_cast(VALUE type_metadata, VALUE value, volatile VALUE* isJson) {
322 | if (value == Qnil || value == Qundef) {
323 | return value;
324 | }
325 |
326 | cache_type_lookup();
327 |
328 | VALUE type_klass, typeCastedValue;
329 |
330 | type_klass = CLASS_OF(type_metadata);
331 | typeCastedValue = Qundef;
332 |
333 | TypeCast typeCast;
334 | for (typeCast = type_casts; typeCast->canCast != NULL; typeCast++) {
335 | if (typeCast->canCast(type_klass)) {
336 | typeCastedValue = typeCast->typeCast(value);
337 | break;
338 | }
339 | }
340 |
341 | if (is_json_type(type_klass)) {
342 | if (is_json_value(value) == Qfalse) {
343 | return Qnil;
344 | }
345 | *isJson = Qtrue;
346 | return value;
347 | }
348 |
349 | if (typeCastedValue == Qundef) {
350 | return rb_funcall(type_metadata, deserialize_from_db_id, 1, value);
351 | }
352 |
353 | return typeCastedValue;
354 | }
355 |
356 | VALUE public_type_cast(int argc, VALUE* argv, VALUE self) {
357 | VALUE type_metadata, value, isJson;
358 | rb_scan_args(argc, argv, "21", &type_metadata, &value, &isJson);
359 |
360 | if (isJson == Qnil || isJson == Qundef) {
361 | isJson = Qfalse;
362 | }
363 |
364 | return type_cast(type_metadata, value, &isJson);
365 | }
366 |
367 | void panko_init_type_cast(VALUE mPanko) {
368 | to_s_id = rb_intern("to_s");
369 | to_i_id = rb_intern("to_i");
370 |
371 | oj_type = rb_const_get_at(rb_cObject, rb_intern("Oj"));
372 | oj_parseerror_type = rb_const_get_at(oj_type, rb_intern("ParseError"));
373 | oj_sc_parse_id = rb_intern("sc_parse");
374 |
375 | // TODO: pass 3 arguments here
376 | rb_define_singleton_method(mPanko, "_type_cast", public_type_cast, -1);
377 |
378 | panko_init_time(mPanko);
379 |
380 | rb_global_variable(&oj_type);
381 | rb_global_variable(&oj_parseerror_type);
382 | rb_global_variable(&ar_string_type);
383 | rb_global_variable(&ar_text_type);
384 | rb_global_variable(&ar_float_type);
385 | rb_global_variable(&ar_integer_type);
386 | rb_global_variable(&ar_boolean_type);
387 | rb_global_variable(&ar_date_time_type);
388 | rb_global_variable(&ar_time_zone_converter);
389 | rb_global_variable(&ar_json_type);
390 | rb_global_variable(&ar_pg_integer_type);
391 | rb_global_variable(&ar_pg_float_type);
392 | rb_global_variable(&ar_pg_uuid_type);
393 | rb_global_variable(&ar_pg_json_type);
394 | rb_global_variable(&ar_pg_jsonb_type);
395 | rb_global_variable(&ar_pg_array_type);
396 | rb_global_variable(&ar_pg_date_time_type);
397 | rb_global_variable(&ar_pg_timestamp_type);
398 | }
399 |
--------------------------------------------------------------------------------
/ext/panko_serializer/attributes_writer/type_cast/type_cast.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | /*
7 | * Type Casting
8 | *
9 | * We do "special" type casting which is mix of two inspirations:
10 | * *) light records gem
11 | * *) pg TextDecoders
12 | *
13 | * The whole idea behind those type casts, are to do the minimum required
14 | * type casting in the most performant manner and *allocation free*.
15 | *
16 | * For example, in `ActiveRecord::Type::String` the type_cast_from_database
17 | * creates new string, for known reasons, but, in serialization flow we don't
18 | * need to create new string becuase we afraid of mutations.
19 | *
20 | * Since we know before hand, that we are only reading from the database, and
21 | * *not* writing and the end result if for JSON we can skip some "defenses".
22 | */
23 |
24 | typedef bool (*TypeMatchFunc)(VALUE type_klass);
25 |
26 | /*
27 | * TypeCastFunc
28 | *
29 | * @return VALUE casted value or Qundef if not casted
30 | */
31 | typedef VALUE (*TypeCastFunc)(VALUE value);
32 |
33 | typedef struct _TypeCast {
34 | TypeMatchFunc canCast;
35 | TypeCastFunc typeCast;
36 | }* TypeCast;
37 |
38 | // ActiveRecord::Type::String
39 | // ActiveRecord::Type::Text
40 | bool is_string_or_text_type(VALUE type_klass);
41 | VALUE cast_string_or_text_type(VALUE value);
42 |
43 | // ActiveRecord::Type::Float
44 | bool is_float_type(VALUE type_klass);
45 | VALUE cast_float_type(VALUE value);
46 |
47 | // ActiveRecord::Type::Integer
48 | bool is_integer_type(VALUE type_klass);
49 | VALUE cast_integer_type(VALUE value);
50 |
51 | // ActiveRecord::ConnectoinAdapters::PostgreSQL::Json
52 | bool is_json_type(VALUE type_klass);
53 | VALUE cast_json_type(VALUE value);
54 |
55 | // ActiveRecord::Type::Boolean
56 | bool is_boolean_type(VALUE type_klass);
57 | VALUE cast_boolean_type(VALUE value);
58 |
59 | // ActiveRecord::Type::DateTime
60 | // ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime
61 | // ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter
62 | bool is_date_time_type(VALUE type_klass);
63 | VALUE cast_date_time_type(VALUE value);
64 |
65 | static struct _TypeCast type_casts[] = {
66 | {is_string_or_text_type, cast_string_or_text_type},
67 | {is_integer_type, cast_integer_type},
68 | {is_boolean_type, cast_boolean_type},
69 | {is_date_time_type, cast_date_time_type},
70 | {is_float_type, cast_float_type},
71 |
72 | {NULL, NULL}};
73 |
74 | extern VALUE type_cast(VALUE type_metadata, VALUE value,
75 | volatile VALUE* isJson);
76 | void panko_init_type_cast(VALUE mPanko);
77 |
78 | // Introduced in ruby 2.4
79 | #ifndef RB_INTEGER_TYPE_P
80 | #define RB_INTEGER_TYPE_P(obj) (RB_FIXNUM_P(obj) || RB_TYPE_P(obj, T_BIGNUM))
81 | #endif
82 |
--------------------------------------------------------------------------------
/ext/panko_serializer/common.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #define PANKO_SAFE_HASH_SIZE(hash) \
6 | (hash == Qnil || hash == Qundef) ? 0 : RHASH_SIZE(hash)
7 |
8 | #define PANKO_EMPTY_HASH(hash) \
9 | (hash == Qnil || hash == Qundef) ? 1 : (RHASH_SIZE(hash) == 0)
10 |
--------------------------------------------------------------------------------
/ext/panko_serializer/extconf.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require "mkmf"
3 | require "pathname"
4 |
5 | $CPPFLAGS += " -Wall"
6 |
7 | extension_name = "panko_serializer"
8 | dir_config(extension_name)
9 |
10 | RbConfig.expand(srcdir = "$(srcdir)".dup)
11 |
12 | # enum all source files
13 | $srcs = Dir[File.join(srcdir, "**/*.c")]
14 |
15 |
16 | # Get all source directories recursivley
17 | directories = Dir[File.join(srcdir, "**/*")].select { |f| File.directory?(f) }
18 | directories = directories.map { |d| Pathname.new(d).relative_path_from(Pathname.new(srcdir)) }
19 | directories.each do |dir|
20 | # add include path to the internal folder
21 | # $(srcdir) is a root folder, where "extconf.rb" is stored
22 | $INCFLAGS << " -I$(srcdir)/#{dir}"
23 |
24 | # add folder, where compiler can search source files
25 | $VPATH << "$(srcdir)/#{dir}"
26 | end
27 |
28 | create_makefile("panko/panko_serializer")
29 |
--------------------------------------------------------------------------------
/ext/panko_serializer/panko_serializer.c:
--------------------------------------------------------------------------------
1 | #include "panko_serializer.h"
2 |
3 | #include
4 |
5 | static ID push_value_id;
6 | static ID push_array_id;
7 | static ID push_object_id;
8 | static ID push_json_id;
9 | static ID pop_id;
10 |
11 | static ID to_a_id;
12 |
13 | static ID object_id;
14 | static ID serialization_context_id;
15 |
16 | static VALUE SKIP = Qundef;
17 |
18 | void write_value(VALUE str_writer, VALUE key, VALUE value, VALUE isJson) {
19 | if (isJson == Qtrue) {
20 | rb_funcall(str_writer, push_json_id, 2, value, key);
21 | } else {
22 | rb_funcall(str_writer, push_value_id, 2, value, key);
23 | }
24 | }
25 |
26 | void serialize_method_fields(VALUE object, VALUE str_writer,
27 | SerializationDescriptor descriptor) {
28 | if (RARRAY_LEN(descriptor->method_fields) == 0) {
29 | return;
30 | }
31 |
32 | volatile VALUE method_fields, serializer, key;
33 | long i;
34 |
35 | method_fields = descriptor->method_fields;
36 |
37 | serializer = descriptor->serializer;
38 | rb_ivar_set(serializer, object_id, object);
39 |
40 | for (i = 0; i < RARRAY_LEN(method_fields); i++) {
41 | volatile VALUE raw_attribute = RARRAY_AREF(method_fields, i);
42 | Attribute attribute = PANKO_ATTRIBUTE_READ(raw_attribute);
43 |
44 | volatile VALUE result = rb_funcall(serializer, attribute->name_id, 0);
45 | if (result != SKIP) {
46 | key = attr_name_for_serialization(attribute);
47 | write_value(str_writer, key, result, Qfalse);
48 | }
49 | }
50 |
51 | rb_ivar_set(serializer, object_id, Qnil);
52 | }
53 |
54 | void serialize_fields(VALUE object, VALUE str_writer,
55 | SerializationDescriptor descriptor) {
56 | descriptor->attributes_writer.write_attributes(object, descriptor->attributes,
57 | write_value, str_writer);
58 |
59 | serialize_method_fields(object, str_writer, descriptor);
60 | }
61 |
62 | void serialize_has_one_associations(VALUE object, VALUE str_writer,
63 | VALUE associations) {
64 | long i;
65 | for (i = 0; i < RARRAY_LEN(associations); i++) {
66 | volatile VALUE association_el = RARRAY_AREF(associations, i);
67 | Association association = association_read(association_el);
68 |
69 | volatile VALUE value = rb_funcall(object, association->name_id, 0);
70 |
71 | if (NIL_P(value)) {
72 | write_value(str_writer, association->name_str, value, Qfalse);
73 | } else {
74 | serialize_object(association->name_str, value, str_writer,
75 | association->descriptor);
76 | }
77 | }
78 | }
79 |
80 | void serialize_has_many_associations(VALUE object, VALUE str_writer,
81 | VALUE associations) {
82 | long i;
83 | for (i = 0; i < RARRAY_LEN(associations); i++) {
84 | volatile VALUE association_el = RARRAY_AREF(associations, i);
85 | Association association = association_read(association_el);
86 |
87 | volatile VALUE value = rb_funcall(object, association->name_id, 0);
88 |
89 | if (NIL_P(value)) {
90 | write_value(str_writer, association->name_str, value, Qfalse);
91 | } else {
92 | serialize_objects(association->name_str, value, str_writer,
93 | association->descriptor);
94 | }
95 | }
96 | }
97 |
98 | VALUE serialize_object(VALUE key, VALUE object, VALUE str_writer,
99 | SerializationDescriptor descriptor) {
100 | sd_set_writer(descriptor, object);
101 |
102 | rb_funcall(str_writer, push_object_id, 1, key);
103 |
104 | serialize_fields(object, str_writer, descriptor);
105 |
106 | if (RARRAY_LEN(descriptor->has_one_associations) > 0) {
107 | serialize_has_one_associations(object, str_writer,
108 | descriptor->has_one_associations);
109 | }
110 |
111 | if (RARRAY_LEN(descriptor->has_many_associations) > 0) {
112 | serialize_has_many_associations(object, str_writer,
113 | descriptor->has_many_associations);
114 | }
115 |
116 | rb_funcall(str_writer, pop_id, 0);
117 |
118 | return Qnil;
119 | }
120 |
121 | VALUE serialize_objects(VALUE key, VALUE objects, VALUE str_writer,
122 | SerializationDescriptor descriptor) {
123 | long i;
124 |
125 | rb_funcall(str_writer, push_array_id, 1, key);
126 |
127 | if (!RB_TYPE_P(objects, T_ARRAY)) {
128 | objects = rb_funcall(objects, to_a_id, 0);
129 | }
130 |
131 | for (i = 0; i < RARRAY_LEN(objects); i++) {
132 | volatile VALUE object = RARRAY_AREF(objects, i);
133 | serialize_object(Qnil, object, str_writer, descriptor);
134 | }
135 |
136 | rb_funcall(str_writer, pop_id, 0);
137 |
138 | return Qnil;
139 | }
140 |
141 | VALUE serialize_object_api(VALUE klass, VALUE object, VALUE str_writer,
142 | VALUE descriptor) {
143 | SerializationDescriptor sd = sd_read(descriptor);
144 | return serialize_object(Qnil, object, str_writer, sd);
145 | }
146 |
147 | VALUE serialize_objects_api(VALUE klass, VALUE objects, VALUE str_writer,
148 | VALUE descriptor) {
149 | serialize_objects(Qnil, objects, str_writer, sd_read(descriptor));
150 |
151 | return Qnil;
152 | }
153 |
154 | void Init_panko_serializer() {
155 | push_value_id = rb_intern("push_value");
156 | push_array_id = rb_intern("push_array");
157 | push_object_id = rb_intern("push_object");
158 | push_json_id = rb_intern("push_json");
159 | pop_id = rb_intern("pop");
160 | to_a_id = rb_intern("to_a");
161 | object_id = rb_intern("@object");
162 | serialization_context_id = rb_intern("@serialization_context");
163 |
164 | VALUE mPanko = rb_define_module("Panko");
165 |
166 | rb_define_singleton_method(mPanko, "serialize_object", serialize_object_api,
167 | 3);
168 |
169 | rb_define_singleton_method(mPanko, "serialize_objects", serialize_objects_api,
170 | 3);
171 |
172 | VALUE mPankoSerializer = rb_const_get(mPanko, rb_intern("Serializer"));
173 | SKIP = rb_const_get(mPankoSerializer, rb_intern("SKIP"));
174 | rb_global_variable(&SKIP);
175 |
176 | panko_init_serialization_descriptor(mPanko);
177 | init_attributes_writer(mPanko);
178 | panko_init_type_cast(mPanko);
179 | panko_init_attribute(mPanko);
180 | panko_init_association(mPanko);
181 | }
182 |
--------------------------------------------------------------------------------
/ext/panko_serializer/panko_serializer.h:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include "attributes_writer/attributes_writer.h"
4 | #include "serialization_descriptor/association.h"
5 | #include "serialization_descriptor/attribute.h"
6 | #include "serialization_descriptor/serialization_descriptor.h"
7 |
8 | VALUE serialize_object(VALUE key, VALUE object, VALUE str_writer,
9 | SerializationDescriptor descriptor);
10 |
11 | VALUE serialize_objects(VALUE key, VALUE objects, VALUE str_writer,
12 | SerializationDescriptor descriptor);
13 |
--------------------------------------------------------------------------------
/ext/panko_serializer/serialization_descriptor/association.c:
--------------------------------------------------------------------------------
1 | #include "association.h"
2 |
3 | VALUE cAssociation;
4 |
5 | static void association_free(void* ptr) {
6 | if (!ptr) {
7 | return;
8 | }
9 |
10 | Association association = (Association)ptr;
11 | association->name_str = Qnil;
12 | association->name_id = 0;
13 | association->name_sym = Qnil;
14 | association->rb_descriptor = Qnil;
15 |
16 | if (!association->descriptor || association->descriptor != NULL) {
17 | association->descriptor = NULL;
18 | }
19 |
20 | xfree(association);
21 | }
22 |
23 | void association_mark(Association data) {
24 | rb_gc_mark(data->name_str);
25 | rb_gc_mark(data->name_sym);
26 | rb_gc_mark(data->rb_descriptor);
27 |
28 | if (data->descriptor != NULL) {
29 | sd_mark(data->descriptor);
30 | }
31 | }
32 |
33 | static VALUE association_new(int argc, VALUE* argv, VALUE self) {
34 | Association association;
35 |
36 | Check_Type(argv[0], T_SYMBOL);
37 | Check_Type(argv[1], T_STRING);
38 |
39 | association = ALLOC(struct _Association);
40 | association->name_sym = argv[0];
41 | association->name_str = argv[1];
42 | association->rb_descriptor = argv[2];
43 |
44 | association->name_id = rb_intern_str(rb_sym2str(association->name_sym));
45 | association->descriptor = sd_read(association->rb_descriptor);
46 |
47 | return Data_Wrap_Struct(cAssociation, association_mark, association_free,
48 | association);
49 | }
50 |
51 | Association association_read(VALUE association) {
52 | return (Association)DATA_PTR(association);
53 | }
54 |
55 | VALUE association_name_sym_ref(VALUE self) {
56 | Association association = (Association)DATA_PTR(self);
57 | return association->name_sym;
58 | }
59 |
60 | VALUE association_name_str_ref(VALUE self) {
61 | Association association = (Association)DATA_PTR(self);
62 | return association->name_str;
63 | }
64 |
65 | VALUE association_descriptor_ref(VALUE self) {
66 | Association association = (Association)DATA_PTR(self);
67 | return association->rb_descriptor;
68 | }
69 |
70 | VALUE association_decriptor_aset(VALUE self, VALUE descriptor) {
71 | Association association = (Association)DATA_PTR(self);
72 |
73 | association->rb_descriptor = descriptor;
74 | association->descriptor = sd_read(descriptor);
75 |
76 | return association->rb_descriptor;
77 | }
78 |
79 | void panko_init_association(VALUE mPanko) {
80 | cAssociation = rb_define_class_under(mPanko, "Association", rb_cObject);
81 | rb_undef_alloc_func(cAssociation);
82 | rb_global_variable(&cAssociation);
83 |
84 | rb_define_module_function(cAssociation, "new", association_new, -1);
85 |
86 | rb_define_method(cAssociation, "name_sym", association_name_sym_ref, 0);
87 | rb_define_method(cAssociation, "name_str", association_name_str_ref, 0);
88 | rb_define_method(cAssociation, "descriptor", association_descriptor_ref, 0);
89 | rb_define_method(cAssociation, "descriptor=", association_decriptor_aset, 1);
90 | }
91 |
--------------------------------------------------------------------------------
/ext/panko_serializer/serialization_descriptor/association.h:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #ifndef __ASSOCIATION_H__
4 | #define __ASSOCIATION_H__
5 |
6 | #include "serialization_descriptor.h"
7 |
8 | typedef struct _Association {
9 | ID name_id;
10 | VALUE name_sym;
11 | VALUE name_str;
12 |
13 | VALUE rb_descriptor;
14 | SerializationDescriptor descriptor;
15 | }* Association;
16 |
17 | Association association_read(VALUE association);
18 | void panko_init_association(VALUE mPanko);
19 |
20 | #endif
21 |
--------------------------------------------------------------------------------
/ext/panko_serializer/serialization_descriptor/attribute.c:
--------------------------------------------------------------------------------
1 | #include "attribute.h"
2 |
3 | ID attribute_aliases_id = 0;
4 | VALUE cAttribute;
5 |
6 | static void attribute_free(void* ptr) {
7 | if (!ptr) {
8 | return;
9 | }
10 |
11 | Attribute attribute = (Attribute)ptr;
12 | attribute->name_str = Qnil;
13 | attribute->name_id = 0;
14 | attribute->alias_name = Qnil;
15 | attribute->type = Qnil;
16 | attribute->record_class = Qnil;
17 |
18 | xfree(attribute);
19 | }
20 |
21 | void attribute_mark(Attribute data) {
22 | rb_gc_mark(data->name_str);
23 | rb_gc_mark(data->alias_name);
24 | rb_gc_mark(data->type);
25 | rb_gc_mark(data->record_class);
26 | }
27 |
28 | static VALUE attribute_new(int argc, VALUE* argv, VALUE self) {
29 | Attribute attribute;
30 |
31 | Check_Type(argv[0], T_STRING);
32 | if (argv[1] != Qnil) {
33 | Check_Type(argv[1], T_STRING);
34 | }
35 |
36 | attribute = ALLOC(struct _Attribute);
37 | attribute->name_str = argv[0];
38 | attribute->name_id = rb_intern_str(attribute->name_str);
39 | attribute->alias_name = argv[1];
40 | attribute->type = Qnil;
41 | attribute->record_class = Qnil;
42 |
43 | return Data_Wrap_Struct(cAttribute, attribute_mark, attribute_free,
44 | attribute);
45 | }
46 |
47 | Attribute attribute_read(VALUE attribute) {
48 | return (Attribute)DATA_PTR(attribute);
49 | }
50 |
51 | void attribute_try_invalidate(Attribute attribute, VALUE new_record_class) {
52 | if (attribute->record_class != new_record_class) {
53 | attribute->type = Qnil;
54 | attribute->record_class = new_record_class;
55 |
56 | // Once the record class is changed for this attribute, check if
57 | // we attribute_aliases (from ActivRecord), if so fill in
58 | // performance wise - this code should be called once (unless the serialzier
59 | // is polymorphic)
60 | volatile VALUE ar_aliases_hash =
61 | rb_funcall(new_record_class, attribute_aliases_id, 0);
62 |
63 | if (!PANKO_EMPTY_HASH(ar_aliases_hash)) {
64 | volatile VALUE aliasedValue =
65 | rb_hash_aref(ar_aliases_hash, attribute->name_str);
66 | if (aliasedValue != Qnil) {
67 | attribute->alias_name = attribute->name_str;
68 | attribute->name_str = aliasedValue;
69 | attribute->name_id = rb_intern_str(attribute->name_str);
70 | }
71 | }
72 | }
73 | }
74 |
75 | VALUE attribute_name_ref(VALUE self) {
76 | Attribute attribute = (Attribute)DATA_PTR(self);
77 | return attribute->name_str;
78 | }
79 |
80 | VALUE attribute_alias_name_ref(VALUE self) {
81 | Attribute attribute = (Attribute)DATA_PTR(self);
82 | return attribute->alias_name;
83 | }
84 |
85 | void panko_init_attribute(VALUE mPanko) {
86 | attribute_aliases_id = rb_intern("attribute_aliases");
87 |
88 | cAttribute = rb_define_class_under(mPanko, "Attribute", rb_cObject);
89 | rb_undef_alloc_func(cAttribute);
90 | rb_global_variable(&cAttribute);
91 |
92 | rb_define_module_function(cAttribute, "new", attribute_new, -1);
93 |
94 | rb_define_method(cAttribute, "name", attribute_name_ref, 0);
95 | rb_define_method(cAttribute, "alias_name", attribute_alias_name_ref, 0);
96 | }
97 |
--------------------------------------------------------------------------------
/ext/panko_serializer/serialization_descriptor/attribute.h:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #ifndef __ATTRIBUTE_H__
4 | #define __ATTRIBUTE_H__
5 |
6 | #include "../common.h"
7 |
8 | typedef struct _Attribute {
9 | VALUE name_str;
10 | ID name_id;
11 | VALUE alias_name;
12 |
13 | /*
14 | * We will cache the activerecord type
15 | * by the record_class
16 | */
17 | VALUE type;
18 | VALUE record_class;
19 | }* Attribute;
20 |
21 | Attribute attribute_read(VALUE attribute);
22 | void attribute_try_invalidate(Attribute attribute, VALUE new_record_class);
23 | void panko_init_attribute(VALUE mPanko);
24 |
25 | #define PANKO_ATTRIBUTE_READ(attribute) (Attribute) DATA_PTR(attribute)
26 |
27 | #endif
28 |
--------------------------------------------------------------------------------
/ext/panko_serializer/serialization_descriptor/serialization_descriptor.c:
--------------------------------------------------------------------------------
1 | #include "serialization_descriptor.h"
2 |
3 | static ID object_id;
4 | static ID sc_id;
5 |
6 | static void sd_free(SerializationDescriptor sd) {
7 | if (!sd) {
8 | return;
9 | }
10 |
11 | sd->serializer = Qnil;
12 | sd->serializer_type = Qnil;
13 | sd->attributes = Qnil;
14 | sd->method_fields = Qnil;
15 | sd->has_one_associations = Qnil;
16 | sd->has_many_associations = Qnil;
17 | sd->aliases = Qnil;
18 | xfree(sd);
19 | }
20 |
21 | void sd_mark(SerializationDescriptor data) {
22 | rb_gc_mark(data->serializer);
23 | rb_gc_mark(data->serializer_type);
24 | rb_gc_mark(data->attributes);
25 | rb_gc_mark(data->method_fields);
26 | rb_gc_mark(data->has_one_associations);
27 | rb_gc_mark(data->has_many_associations);
28 | rb_gc_mark(data->aliases);
29 | }
30 |
31 | static VALUE sd_alloc(VALUE klass) {
32 | SerializationDescriptor sd = ALLOC(struct _SerializationDescriptor);
33 |
34 | sd->serializer = Qnil;
35 | sd->serializer_type = Qnil;
36 | sd->attributes = Qnil;
37 | sd->method_fields = Qnil;
38 | sd->has_one_associations = Qnil;
39 | sd->has_many_associations = Qnil;
40 | sd->aliases = Qnil;
41 |
42 | sd->attributes_writer = create_empty_attributes_writer();
43 |
44 | return Data_Wrap_Struct(klass, sd_mark, sd_free, sd);
45 | }
46 |
47 | SerializationDescriptor sd_read(VALUE descriptor) {
48 | return (SerializationDescriptor)DATA_PTR(descriptor);
49 | }
50 |
51 | void sd_set_writer(SerializationDescriptor sd, VALUE object) {
52 | if (sd->attributes_writer.object_type != UnknownObjectType) {
53 | return;
54 | }
55 |
56 | sd->attributes_writer = create_attributes_writer(object);
57 | }
58 |
59 | VALUE sd_serializer_set(VALUE self, VALUE serializer) {
60 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
61 |
62 | sd->serializer = serializer;
63 | return Qnil;
64 | }
65 |
66 | VALUE sd_serializer_ref(VALUE self) {
67 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
68 |
69 | return sd->serializer;
70 | }
71 |
72 | VALUE sd_attributes_set(VALUE self, VALUE attributes) {
73 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
74 |
75 | sd->attributes = attributes;
76 | return Qnil;
77 | }
78 |
79 | VALUE sd_attributes_ref(VALUE self) {
80 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
81 | return sd->attributes;
82 | }
83 |
84 | VALUE sd_method_fields_set(VALUE self, VALUE method_fields) {
85 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
86 | sd->method_fields = method_fields;
87 | return Qnil;
88 | }
89 |
90 | VALUE sd_method_fields_ref(VALUE self) {
91 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
92 | return sd->method_fields;
93 | }
94 |
95 | VALUE sd_has_one_associations_set(VALUE self, VALUE has_one_associations) {
96 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
97 | sd->has_one_associations = has_one_associations;
98 | return Qnil;
99 | }
100 |
101 | VALUE sd_has_one_associations_ref(VALUE self) {
102 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
103 | return sd->has_one_associations;
104 | }
105 |
106 | VALUE sd_has_many_associations_set(VALUE self, VALUE has_many_associations) {
107 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
108 | sd->has_many_associations = has_many_associations;
109 | return Qnil;
110 | }
111 |
112 | VALUE sd_has_many_associations_ref(VALUE self) {
113 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
114 | return sd->has_many_associations;
115 | }
116 |
117 | VALUE sd_type_set(VALUE self, VALUE type) {
118 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
119 | sd->serializer_type = type;
120 | return Qnil;
121 | }
122 |
123 | VALUE sd_type_aref(VALUE self) {
124 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
125 | return sd->serializer_type;
126 | }
127 |
128 | VALUE sd_aliases_set(VALUE self, VALUE aliases) {
129 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
130 | sd->aliases = aliases;
131 | return Qnil;
132 | }
133 |
134 | VALUE sd_aliases_aref(VALUE self) {
135 | SerializationDescriptor sd = (SerializationDescriptor)DATA_PTR(self);
136 | return sd->aliases;
137 | }
138 |
139 | void panko_init_serialization_descriptor(VALUE mPanko) {
140 | object_id = rb_intern("@object");
141 | sc_id = rb_intern("@sc");
142 |
143 | VALUE cSerializationDescriptor =
144 | rb_define_class_under(mPanko, "SerializationDescriptor", rb_cObject);
145 |
146 | rb_define_alloc_func(cSerializationDescriptor, sd_alloc);
147 | rb_define_method(cSerializationDescriptor, "serializer=", sd_serializer_set,
148 | 1);
149 | rb_define_method(cSerializationDescriptor, "serializer", sd_serializer_ref,
150 | 0);
151 |
152 | rb_define_method(cSerializationDescriptor, "attributes=", sd_attributes_set,
153 | 1);
154 | rb_define_method(cSerializationDescriptor, "attributes", sd_attributes_ref,
155 | 0);
156 |
157 | rb_define_method(cSerializationDescriptor,
158 | "method_fields=", sd_method_fields_set, 1);
159 | rb_define_method(cSerializationDescriptor, "method_fields",
160 | sd_method_fields_ref, 0);
161 |
162 | rb_define_method(cSerializationDescriptor,
163 | "has_one_associations=", sd_has_one_associations_set, 1);
164 | rb_define_method(cSerializationDescriptor, "has_one_associations",
165 | sd_has_one_associations_ref, 0);
166 |
167 | rb_define_method(cSerializationDescriptor,
168 | "has_many_associations=", sd_has_many_associations_set, 1);
169 | rb_define_method(cSerializationDescriptor, "has_many_associations",
170 | sd_has_many_associations_ref, 0);
171 |
172 | rb_define_method(cSerializationDescriptor, "type=", sd_type_set, 1);
173 | rb_define_method(cSerializationDescriptor, "type", sd_type_aref, 0);
174 |
175 | rb_define_method(cSerializationDescriptor, "aliases=", sd_aliases_set, 1);
176 | rb_define_method(cSerializationDescriptor, "aliases", sd_aliases_aref, 0);
177 | }
178 |
--------------------------------------------------------------------------------
/ext/panko_serializer/serialization_descriptor/serialization_descriptor.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | #include "attributes_writer/attributes_writer.h"
7 |
8 | typedef struct _SerializationDescriptor {
9 | // type of the serializer, so we can create it later
10 | VALUE serializer_type;
11 | // Cached value of the serializer
12 | VALUE serializer;
13 |
14 | // Metadata
15 | VALUE attributes;
16 | VALUE aliases;
17 | VALUE method_fields;
18 | VALUE has_one_associations;
19 | VALUE has_many_associations;
20 |
21 | AttributesWriter attributes_writer;
22 | }* SerializationDescriptor;
23 |
24 | SerializationDescriptor sd_read(VALUE descriptor);
25 |
26 | void sd_mark(SerializationDescriptor data);
27 |
28 | void sd_set_writer(SerializationDescriptor sd, VALUE object);
29 |
30 | void panko_init_serialization_descriptor(VALUE mPanko);
31 |
--------------------------------------------------------------------------------
/gemfiles/7.0.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "sqlite3", "~> 1.4"
6 | gem "activesupport", "~> 7.0.0"
7 | gem "activemodel", "~> 7.0.0"
8 | gem "activerecord", "~> 7.0.0", group: :test
9 |
10 | group :benchmarks do
11 | gem "vernier"
12 | gem "stackprof"
13 | gem "pg"
14 | gem "benchmark-ips"
15 | gem "active_model_serializers", "~> 0.10"
16 | gem "terminal-table"
17 | gem "memory_profiler"
18 | end
19 |
20 | group :test do
21 | gem "faker"
22 | end
23 |
24 | group :development do
25 | gem "byebug"
26 | gem "rake"
27 | gem "rspec", "~> 3.0"
28 | gem "rake-compiler"
29 | end
30 |
31 | group :development, :test do
32 | gem "rubocop"
33 | gem "standard"
34 | gem "standard-performance"
35 | gem "rubocop-performance"
36 | gem "rubocop-rspec"
37 | end
38 |
39 | gemspec path: "../"
40 |
--------------------------------------------------------------------------------
/gemfiles/7.0.0.gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: ..
3 | specs:
4 | panko_serializer (0.8.3)
5 | activesupport
6 | oj (> 3.11.0, < 4.0.0)
7 |
8 | GEM
9 | remote: https://rubygems.org/
10 | specs:
11 | actionpack (7.0.8.5)
12 | actionview (= 7.0.8.5)
13 | activesupport (= 7.0.8.5)
14 | rack (~> 2.0, >= 2.2.4)
15 | rack-test (>= 0.6.3)
16 | rails-dom-testing (~> 2.0)
17 | rails-html-sanitizer (~> 1.0, >= 1.2.0)
18 | actionview (7.0.8.5)
19 | activesupport (= 7.0.8.5)
20 | builder (~> 3.1)
21 | erubi (~> 1.4)
22 | rails-dom-testing (~> 2.0)
23 | rails-html-sanitizer (~> 1.1, >= 1.2.0)
24 | active_model_serializers (0.10.14)
25 | actionpack (>= 4.1)
26 | activemodel (>= 4.1)
27 | case_transform (>= 0.2)
28 | jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
29 | activemodel (7.0.8.5)
30 | activesupport (= 7.0.8.5)
31 | activerecord (7.0.8.5)
32 | activemodel (= 7.0.8.5)
33 | activesupport (= 7.0.8.5)
34 | activesupport (7.0.8.5)
35 | concurrent-ruby (~> 1.0, >= 1.0.2)
36 | i18n (>= 1.6, < 2)
37 | minitest (>= 5.1)
38 | tzinfo (~> 2.0)
39 | appraisal (2.5.0)
40 | bundler
41 | rake
42 | thor (>= 0.14.0)
43 | ast (2.4.2)
44 | benchmark-ips (2.14.0)
45 | bigdecimal (3.1.8)
46 | builder (3.3.0)
47 | byebug (11.1.3)
48 | case_transform (0.2)
49 | activesupport
50 | concurrent-ruby (1.3.4)
51 | crass (1.0.6)
52 | diff-lcs (1.5.1)
53 | erubi (1.13.0)
54 | faker (3.5.1)
55 | i18n (>= 1.8.11, < 2)
56 | i18n (1.14.6)
57 | concurrent-ruby (~> 1.0)
58 | json (2.7.5)
59 | jsonapi-renderer (0.2.2)
60 | language_server-protocol (3.17.0.3)
61 | lint_roller (1.1.0)
62 | loofah (2.22.0)
63 | crass (~> 1.0.2)
64 | nokogiri (>= 1.12.0)
65 | memory_profiler (1.1.0)
66 | minitest (5.25.4)
67 | nokogiri (1.16.7-arm64-darwin)
68 | racc (~> 1.4)
69 | oj (3.16.7)
70 | bigdecimal (>= 3.0)
71 | ostruct (>= 0.2)
72 | ostruct (0.6.0)
73 | parallel (1.26.3)
74 | parser (3.3.5.1)
75 | ast (~> 2.4.1)
76 | racc
77 | pg (1.5.9)
78 | racc (1.8.1)
79 | rack (2.2.10)
80 | rack-test (2.1.0)
81 | rack (>= 1.3)
82 | rails-dom-testing (2.2.0)
83 | activesupport (>= 5.0.0)
84 | minitest
85 | nokogiri (>= 1.6)
86 | rails-html-sanitizer (1.6.0)
87 | loofah (~> 2.21)
88 | nokogiri (~> 1.14)
89 | rainbow (3.1.1)
90 | rake (13.2.1)
91 | rake-compiler (1.2.8)
92 | rake
93 | regexp_parser (2.9.2)
94 | rspec (3.13.0)
95 | rspec-core (~> 3.13.0)
96 | rspec-expectations (~> 3.13.0)
97 | rspec-mocks (~> 3.13.0)
98 | rspec-core (3.13.2)
99 | rspec-support (~> 3.13.0)
100 | rspec-expectations (3.13.3)
101 | diff-lcs (>= 1.2.0, < 2.0)
102 | rspec-support (~> 3.13.0)
103 | rspec-mocks (3.13.2)
104 | diff-lcs (>= 1.2.0, < 2.0)
105 | rspec-support (~> 3.13.0)
106 | rspec-support (3.13.1)
107 | rubocop (1.66.1)
108 | json (~> 2.3)
109 | language_server-protocol (>= 3.17.0)
110 | parallel (~> 1.10)
111 | parser (>= 3.3.0.2)
112 | rainbow (>= 2.2.2, < 4.0)
113 | regexp_parser (>= 2.4, < 3.0)
114 | rubocop-ast (>= 1.32.2, < 2.0)
115 | ruby-progressbar (~> 1.7)
116 | unicode-display_width (>= 2.4.0, < 3.0)
117 | rubocop-ast (1.33.0)
118 | parser (>= 3.3.1.0)
119 | rubocop-performance (1.22.1)
120 | rubocop (>= 1.48.1, < 2.0)
121 | rubocop-ast (>= 1.31.1, < 2.0)
122 | rubocop-rspec (3.2.0)
123 | rubocop (~> 1.61)
124 | ruby-progressbar (1.13.0)
125 | sqlite3 (1.7.3-arm64-darwin)
126 | stackprof (0.2.26)
127 | standard (1.41.1)
128 | language_server-protocol (~> 3.17.0.2)
129 | lint_roller (~> 1.0)
130 | rubocop (~> 1.66.0)
131 | standard-custom (~> 1.0.0)
132 | standard-performance (~> 1.5)
133 | standard-custom (1.0.2)
134 | lint_roller (~> 1.0)
135 | rubocop (~> 1.50)
136 | standard-performance (1.5.0)
137 | lint_roller (~> 1.1)
138 | rubocop-performance (~> 1.22.0)
139 | terminal-table (3.0.2)
140 | unicode-display_width (>= 1.1.1, < 3)
141 | thor (1.3.2)
142 | tzinfo (2.0.6)
143 | concurrent-ruby (~> 1.0)
144 | unicode-display_width (2.6.0)
145 | vernier (1.3.0)
146 |
147 | PLATFORMS
148 | arm64-darwin-23
149 |
150 | DEPENDENCIES
151 | active_model_serializers (~> 0.10)
152 | activemodel (~> 7.0.0)
153 | activerecord (~> 7.0.0)
154 | activesupport (~> 7.0.0)
155 | appraisal
156 | benchmark-ips
157 | byebug
158 | faker
159 | memory_profiler
160 | panko_serializer!
161 | pg
162 | rake
163 | rake-compiler
164 | rspec (~> 3.0)
165 | rubocop
166 | rubocop-performance
167 | rubocop-rspec
168 | sqlite3 (~> 1.4)
169 | stackprof
170 | standard
171 | standard-performance
172 | terminal-table
173 | vernier
174 |
175 | BUNDLED WITH
176 | 2.4.6
177 |
--------------------------------------------------------------------------------
/gemfiles/7.1.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "sqlite3", "~> 1.4"
6 | gem "activesupport", "~> 7.1.5"
7 | gem "activemodel", "~> 7.1.5"
8 | gem "activerecord", "~> 7.1.5", group: :test
9 |
10 | group :benchmarks do
11 | gem "vernier"
12 | gem "stackprof"
13 | gem "pg"
14 | gem "benchmark-ips"
15 | gem "active_model_serializers", "~> 0.10"
16 | gem "terminal-table"
17 | gem "memory_profiler"
18 | end
19 |
20 | group :test do
21 | gem "faker"
22 | end
23 |
24 | group :development do
25 | gem "byebug"
26 | gem "rake"
27 | gem "rspec", "~> 3.0"
28 | gem "rake-compiler"
29 | end
30 |
31 | group :development, :test do
32 | gem "rubocop"
33 | gem "standard"
34 | gem "standard-performance"
35 | gem "rubocop-performance"
36 | gem "rubocop-rspec"
37 | end
38 |
39 | gemspec path: "../"
40 |
--------------------------------------------------------------------------------
/gemfiles/7.1.0.gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: ..
3 | specs:
4 | panko_serializer (0.8.3)
5 | activesupport
6 | oj (> 3.11.0, < 4.0.0)
7 |
8 | GEM
9 | remote: https://rubygems.org/
10 | specs:
11 | actionpack (7.1.5.1)
12 | actionview (= 7.1.5.1)
13 | activesupport (= 7.1.5.1)
14 | nokogiri (>= 1.8.5)
15 | racc
16 | rack (>= 2.2.4)
17 | rack-session (>= 1.0.1)
18 | rack-test (>= 0.6.3)
19 | rails-dom-testing (~> 2.2)
20 | rails-html-sanitizer (~> 1.6)
21 | actionview (7.1.5.1)
22 | activesupport (= 7.1.5.1)
23 | builder (~> 3.1)
24 | erubi (~> 1.11)
25 | rails-dom-testing (~> 2.2)
26 | rails-html-sanitizer (~> 1.6)
27 | active_model_serializers (0.10.14)
28 | actionpack (>= 4.1)
29 | activemodel (>= 4.1)
30 | case_transform (>= 0.2)
31 | jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
32 | activemodel (7.1.5.1)
33 | activesupport (= 7.1.5.1)
34 | activerecord (7.1.5.1)
35 | activemodel (= 7.1.5.1)
36 | activesupport (= 7.1.5.1)
37 | timeout (>= 0.4.0)
38 | activesupport (7.1.5.1)
39 | base64
40 | benchmark (>= 0.3)
41 | bigdecimal
42 | concurrent-ruby (~> 1.0, >= 1.0.2)
43 | connection_pool (>= 2.2.5)
44 | drb
45 | i18n (>= 1.6, < 2)
46 | logger (>= 1.4.2)
47 | minitest (>= 5.1)
48 | mutex_m
49 | securerandom (>= 0.3)
50 | tzinfo (~> 2.0)
51 | appraisal (2.5.0)
52 | bundler
53 | rake
54 | thor (>= 0.14.0)
55 | ast (2.4.2)
56 | base64 (0.2.0)
57 | benchmark (0.4.0)
58 | benchmark-ips (2.14.0)
59 | bigdecimal (3.1.8)
60 | builder (3.3.0)
61 | byebug (11.1.3)
62 | case_transform (0.2)
63 | activesupport
64 | concurrent-ruby (1.3.4)
65 | connection_pool (2.4.1)
66 | crass (1.0.6)
67 | diff-lcs (1.5.1)
68 | drb (2.2.1)
69 | erubi (1.13.0)
70 | faker (3.5.1)
71 | i18n (>= 1.8.11, < 2)
72 | i18n (1.14.6)
73 | concurrent-ruby (~> 1.0)
74 | json (2.7.5)
75 | jsonapi-renderer (0.2.2)
76 | language_server-protocol (3.17.0.3)
77 | lint_roller (1.1.0)
78 | logger (1.6.3)
79 | loofah (2.23.1)
80 | crass (~> 1.0.2)
81 | nokogiri (>= 1.12.0)
82 | memory_profiler (1.1.0)
83 | minitest (5.25.4)
84 | mutex_m (0.3.0)
85 | nokogiri (1.16.7-arm64-darwin)
86 | racc (~> 1.4)
87 | oj (3.16.7)
88 | bigdecimal (>= 3.0)
89 | ostruct (>= 0.2)
90 | ostruct (0.6.0)
91 | parallel (1.26.3)
92 | parser (3.3.5.1)
93 | ast (~> 2.4.1)
94 | racc
95 | pg (1.5.9)
96 | racc (1.8.1)
97 | rack (3.1.8)
98 | rack-session (2.0.0)
99 | rack (>= 3.0.0)
100 | rack-test (2.1.0)
101 | rack (>= 1.3)
102 | rails-dom-testing (2.2.0)
103 | activesupport (>= 5.0.0)
104 | minitest
105 | nokogiri (>= 1.6)
106 | rails-html-sanitizer (1.6.0)
107 | loofah (~> 2.21)
108 | nokogiri (~> 1.14)
109 | rainbow (3.1.1)
110 | rake (13.2.1)
111 | rake-compiler (1.2.8)
112 | rake
113 | regexp_parser (2.9.2)
114 | rspec (3.13.0)
115 | rspec-core (~> 3.13.0)
116 | rspec-expectations (~> 3.13.0)
117 | rspec-mocks (~> 3.13.0)
118 | rspec-core (3.13.2)
119 | rspec-support (~> 3.13.0)
120 | rspec-expectations (3.13.3)
121 | diff-lcs (>= 1.2.0, < 2.0)
122 | rspec-support (~> 3.13.0)
123 | rspec-mocks (3.13.2)
124 | diff-lcs (>= 1.2.0, < 2.0)
125 | rspec-support (~> 3.13.0)
126 | rspec-support (3.13.1)
127 | rubocop (1.66.1)
128 | json (~> 2.3)
129 | language_server-protocol (>= 3.17.0)
130 | parallel (~> 1.10)
131 | parser (>= 3.3.0.2)
132 | rainbow (>= 2.2.2, < 4.0)
133 | regexp_parser (>= 2.4, < 3.0)
134 | rubocop-ast (>= 1.32.2, < 2.0)
135 | ruby-progressbar (~> 1.7)
136 | unicode-display_width (>= 2.4.0, < 3.0)
137 | rubocop-ast (1.33.0)
138 | parser (>= 3.3.1.0)
139 | rubocop-performance (1.22.1)
140 | rubocop (>= 1.48.1, < 2.0)
141 | rubocop-ast (>= 1.31.1, < 2.0)
142 | rubocop-rspec (3.2.0)
143 | rubocop (~> 1.61)
144 | ruby-progressbar (1.13.0)
145 | securerandom (0.4.0)
146 | sqlite3 (1.7.3-arm64-darwin)
147 | stackprof (0.2.26)
148 | standard (1.41.1)
149 | language_server-protocol (~> 3.17.0.2)
150 | lint_roller (~> 1.0)
151 | rubocop (~> 1.66.0)
152 | standard-custom (~> 1.0.0)
153 | standard-performance (~> 1.5)
154 | standard-custom (1.0.2)
155 | lint_roller (~> 1.0)
156 | rubocop (~> 1.50)
157 | standard-performance (1.5.0)
158 | lint_roller (~> 1.1)
159 | rubocop-performance (~> 1.22.0)
160 | terminal-table (3.0.2)
161 | unicode-display_width (>= 1.1.1, < 3)
162 | thor (1.3.2)
163 | timeout (0.4.2)
164 | tzinfo (2.0.6)
165 | concurrent-ruby (~> 1.0)
166 | unicode-display_width (2.6.0)
167 | vernier (1.3.0)
168 |
169 | PLATFORMS
170 | arm64-darwin
171 |
172 | DEPENDENCIES
173 | active_model_serializers (~> 0.10)
174 | activemodel (~> 7.1.5)
175 | activerecord (~> 7.1.5)
176 | activesupport (~> 7.1.5)
177 | appraisal
178 | benchmark-ips
179 | byebug
180 | faker
181 | memory_profiler
182 | panko_serializer!
183 | pg
184 | rake
185 | rake-compiler
186 | rspec (~> 3.0)
187 | rubocop
188 | rubocop-performance
189 | rubocop-rspec
190 | sqlite3 (~> 1.4)
191 | stackprof
192 | standard
193 | standard-performance
194 | terminal-table
195 | vernier
196 |
197 | BUNDLED WITH
198 | 2.5.21
199 |
--------------------------------------------------------------------------------
/gemfiles/7.2.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "sqlite3", "~> 1.4"
6 | gem "activesupport", "~> 7.2.0"
7 | gem "activemodel", "~> 7.2.0"
8 | gem "activerecord", "~> 7.2.0", group: :test
9 |
10 | group :benchmarks do
11 | gem "vernier"
12 | gem "stackprof"
13 | gem "pg"
14 | gem "benchmark-ips"
15 | gem "active_model_serializers", "~> 0.10"
16 | gem "terminal-table"
17 | gem "memory_profiler"
18 | end
19 |
20 | group :test do
21 | gem "faker"
22 | end
23 |
24 | group :development do
25 | gem "byebug"
26 | gem "rake"
27 | gem "rspec", "~> 3.0"
28 | gem "rake-compiler"
29 | end
30 |
31 | group :development, :test do
32 | gem "rubocop"
33 | gem "standard"
34 | gem "standard-performance"
35 | gem "rubocop-performance"
36 | gem "rubocop-rspec"
37 | end
38 |
39 | gemspec path: "../"
40 |
--------------------------------------------------------------------------------
/gemfiles/7.2.0.gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: ..
3 | specs:
4 | panko_serializer (0.8.3)
5 | activesupport
6 | oj (> 3.11.0, < 4.0.0)
7 |
8 | GEM
9 | remote: https://rubygems.org/
10 | specs:
11 | actionpack (7.2.2.1)
12 | actionview (= 7.2.2.1)
13 | activesupport (= 7.2.2.1)
14 | nokogiri (>= 1.8.5)
15 | racc
16 | rack (>= 2.2.4, < 3.2)
17 | rack-session (>= 1.0.1)
18 | rack-test (>= 0.6.3)
19 | rails-dom-testing (~> 2.2)
20 | rails-html-sanitizer (~> 1.6)
21 | useragent (~> 0.16)
22 | actionview (7.2.2.1)
23 | activesupport (= 7.2.2.1)
24 | builder (~> 3.1)
25 | erubi (~> 1.11)
26 | rails-dom-testing (~> 2.2)
27 | rails-html-sanitizer (~> 1.6)
28 | active_model_serializers (0.10.14)
29 | actionpack (>= 4.1)
30 | activemodel (>= 4.1)
31 | case_transform (>= 0.2)
32 | jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
33 | activemodel (7.2.2.1)
34 | activesupport (= 7.2.2.1)
35 | activerecord (7.2.2.1)
36 | activemodel (= 7.2.2.1)
37 | activesupport (= 7.2.2.1)
38 | timeout (>= 0.4.0)
39 | activesupport (7.2.2.1)
40 | base64
41 | benchmark (>= 0.3)
42 | bigdecimal
43 | concurrent-ruby (~> 1.0, >= 1.3.1)
44 | connection_pool (>= 2.2.5)
45 | drb
46 | i18n (>= 1.6, < 2)
47 | logger (>= 1.4.2)
48 | minitest (>= 5.1)
49 | securerandom (>= 0.3)
50 | tzinfo (~> 2.0, >= 2.0.5)
51 | appraisal (2.5.0)
52 | bundler
53 | rake
54 | thor (>= 0.14.0)
55 | ast (2.4.2)
56 | base64 (0.2.0)
57 | benchmark (0.4.0)
58 | benchmark-ips (2.14.0)
59 | bigdecimal (3.1.8)
60 | builder (3.3.0)
61 | byebug (11.1.3)
62 | case_transform (0.2)
63 | activesupport
64 | concurrent-ruby (1.3.4)
65 | connection_pool (2.4.1)
66 | crass (1.0.6)
67 | diff-lcs (1.5.1)
68 | drb (2.2.1)
69 | erubi (1.13.0)
70 | faker (3.5.1)
71 | i18n (>= 1.8.11, < 2)
72 | i18n (1.14.6)
73 | concurrent-ruby (~> 1.0)
74 | json (2.7.5)
75 | jsonapi-renderer (0.2.2)
76 | language_server-protocol (3.17.0.3)
77 | lint_roller (1.1.0)
78 | logger (1.6.3)
79 | loofah (2.22.0)
80 | crass (~> 1.0.2)
81 | nokogiri (>= 1.12.0)
82 | memory_profiler (1.1.0)
83 | minitest (5.25.4)
84 | nokogiri (1.16.7-arm64-darwin)
85 | racc (~> 1.4)
86 | oj (3.16.7)
87 | bigdecimal (>= 3.0)
88 | ostruct (>= 0.2)
89 | ostruct (0.6.0)
90 | parallel (1.26.3)
91 | parser (3.3.5.1)
92 | ast (~> 2.4.1)
93 | racc
94 | pg (1.5.9)
95 | racc (1.8.1)
96 | rack (3.1.8)
97 | rack-session (2.0.0)
98 | rack (>= 3.0.0)
99 | rack-test (2.1.0)
100 | rack (>= 1.3)
101 | rails-dom-testing (2.2.0)
102 | activesupport (>= 5.0.0)
103 | minitest
104 | nokogiri (>= 1.6)
105 | rails-html-sanitizer (1.6.0)
106 | loofah (~> 2.21)
107 | nokogiri (~> 1.14)
108 | rainbow (3.1.1)
109 | rake (13.2.1)
110 | rake-compiler (1.2.8)
111 | rake
112 | regexp_parser (2.9.2)
113 | rspec (3.13.0)
114 | rspec-core (~> 3.13.0)
115 | rspec-expectations (~> 3.13.0)
116 | rspec-mocks (~> 3.13.0)
117 | rspec-core (3.13.2)
118 | rspec-support (~> 3.13.0)
119 | rspec-expectations (3.13.3)
120 | diff-lcs (>= 1.2.0, < 2.0)
121 | rspec-support (~> 3.13.0)
122 | rspec-mocks (3.13.2)
123 | diff-lcs (>= 1.2.0, < 2.0)
124 | rspec-support (~> 3.13.0)
125 | rspec-support (3.13.1)
126 | rubocop (1.66.1)
127 | json (~> 2.3)
128 | language_server-protocol (>= 3.17.0)
129 | parallel (~> 1.10)
130 | parser (>= 3.3.0.2)
131 | rainbow (>= 2.2.2, < 4.0)
132 | regexp_parser (>= 2.4, < 3.0)
133 | rubocop-ast (>= 1.32.2, < 2.0)
134 | ruby-progressbar (~> 1.7)
135 | unicode-display_width (>= 2.4.0, < 3.0)
136 | rubocop-ast (1.33.0)
137 | parser (>= 3.3.1.0)
138 | rubocop-performance (1.22.1)
139 | rubocop (>= 1.48.1, < 2.0)
140 | rubocop-ast (>= 1.31.1, < 2.0)
141 | rubocop-rspec (3.2.0)
142 | rubocop (~> 1.61)
143 | ruby-progressbar (1.13.0)
144 | securerandom (0.4.0)
145 | sqlite3 (1.7.3-arm64-darwin)
146 | stackprof (0.2.26)
147 | standard (1.41.1)
148 | language_server-protocol (~> 3.17.0.2)
149 | lint_roller (~> 1.0)
150 | rubocop (~> 1.66.0)
151 | standard-custom (~> 1.0.0)
152 | standard-performance (~> 1.5)
153 | standard-custom (1.0.2)
154 | lint_roller (~> 1.0)
155 | rubocop (~> 1.50)
156 | standard-performance (1.5.0)
157 | lint_roller (~> 1.1)
158 | rubocop-performance (~> 1.22.0)
159 | terminal-table (3.0.2)
160 | unicode-display_width (>= 1.1.1, < 3)
161 | thor (1.3.2)
162 | timeout (0.4.2)
163 | tzinfo (2.0.6)
164 | concurrent-ruby (~> 1.0)
165 | unicode-display_width (2.6.0)
166 | useragent (0.16.10)
167 | vernier (1.3.0)
168 |
169 | PLATFORMS
170 | arm64-darwin
171 |
172 | DEPENDENCIES
173 | active_model_serializers (~> 0.10)
174 | activemodel (~> 7.2.0)
175 | activerecord (~> 7.2.0)
176 | activesupport (~> 7.2.0)
177 | appraisal
178 | benchmark-ips
179 | byebug
180 | faker
181 | memory_profiler
182 | panko_serializer!
183 | pg
184 | rake
185 | rake-compiler
186 | rspec (~> 3.0)
187 | rubocop
188 | rubocop-performance
189 | rubocop-rspec
190 | sqlite3 (~> 1.4)
191 | stackprof
192 | standard
193 | standard-performance
194 | terminal-table
195 | vernier
196 |
197 | BUNDLED WITH
198 | 2.5.21
199 |
--------------------------------------------------------------------------------
/gemfiles/8.0.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "sqlite3", ">= 2.1"
6 | gem "activesupport", "~> 8.0.0"
7 | gem "activemodel", "~> 8.0.0"
8 | gem "activerecord", "~> 8.0.0", group: :test
9 |
10 | group :benchmarks do
11 | gem "vernier"
12 | gem "stackprof"
13 | gem "pg"
14 | gem "benchmark-ips"
15 | gem "active_model_serializers", "~> 0.10"
16 | gem "terminal-table"
17 | gem "memory_profiler"
18 | end
19 |
20 | group :test do
21 | gem "faker"
22 | end
23 |
24 | group :development do
25 | gem "byebug"
26 | gem "rake"
27 | gem "rspec", "~> 3.0"
28 | gem "rake-compiler"
29 | end
30 |
31 | group :development, :test do
32 | gem "rubocop"
33 | gem "standard"
34 | gem "standard-performance"
35 | gem "rubocop-performance"
36 | gem "rubocop-rspec"
37 | end
38 |
39 | gemspec path: "../"
40 |
--------------------------------------------------------------------------------
/gemfiles/8.0.0.gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: ..
3 | specs:
4 | panko_serializer (0.8.3)
5 | activesupport
6 | oj (> 3.11.0, < 4.0.0)
7 |
8 | GEM
9 | remote: https://rubygems.org/
10 | specs:
11 | actionpack (8.0.1)
12 | actionview (= 8.0.1)
13 | activesupport (= 8.0.1)
14 | nokogiri (>= 1.8.5)
15 | rack (>= 2.2.4)
16 | rack-session (>= 1.0.1)
17 | rack-test (>= 0.6.3)
18 | rails-dom-testing (~> 2.2)
19 | rails-html-sanitizer (~> 1.6)
20 | useragent (~> 0.16)
21 | actionview (8.0.1)
22 | activesupport (= 8.0.1)
23 | builder (~> 3.1)
24 | erubi (~> 1.11)
25 | rails-dom-testing (~> 2.2)
26 | rails-html-sanitizer (~> 1.6)
27 | active_model_serializers (0.10.14)
28 | actionpack (>= 4.1)
29 | activemodel (>= 4.1)
30 | case_transform (>= 0.2)
31 | jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
32 | activemodel (8.0.1)
33 | activesupport (= 8.0.1)
34 | activerecord (8.0.1)
35 | activemodel (= 8.0.1)
36 | activesupport (= 8.0.1)
37 | timeout (>= 0.4.0)
38 | activesupport (8.0.1)
39 | base64
40 | benchmark (>= 0.3)
41 | bigdecimal
42 | concurrent-ruby (~> 1.0, >= 1.3.1)
43 | connection_pool (>= 2.2.5)
44 | drb
45 | i18n (>= 1.6, < 2)
46 | logger (>= 1.4.2)
47 | minitest (>= 5.1)
48 | securerandom (>= 0.3)
49 | tzinfo (~> 2.0, >= 2.0.5)
50 | uri (>= 0.13.1)
51 | appraisal (2.5.0)
52 | bundler
53 | rake
54 | thor (>= 0.14.0)
55 | ast (2.4.2)
56 | base64 (0.2.0)
57 | benchmark (0.4.0)
58 | benchmark-ips (2.14.0)
59 | bigdecimal (3.1.8)
60 | builder (3.3.0)
61 | byebug (11.1.3)
62 | case_transform (0.2)
63 | activesupport
64 | concurrent-ruby (1.3.4)
65 | connection_pool (2.4.1)
66 | crass (1.0.6)
67 | diff-lcs (1.5.1)
68 | drb (2.2.1)
69 | erubi (1.13.0)
70 | faker (3.5.1)
71 | i18n (>= 1.8.11, < 2)
72 | i18n (1.14.6)
73 | concurrent-ruby (~> 1.0)
74 | json (2.7.5)
75 | jsonapi-renderer (0.2.2)
76 | language_server-protocol (3.17.0.3)
77 | lint_roller (1.1.0)
78 | logger (1.6.3)
79 | loofah (2.22.0)
80 | crass (~> 1.0.2)
81 | nokogiri (>= 1.12.0)
82 | memory_profiler (1.1.0)
83 | minitest (5.25.4)
84 | nokogiri (1.16.7-aarch64-linux)
85 | racc (~> 1.4)
86 | nokogiri (1.16.7-arm-linux)
87 | racc (~> 1.4)
88 | nokogiri (1.16.7-arm64-darwin)
89 | racc (~> 1.4)
90 | nokogiri (1.16.7-x86-linux)
91 | racc (~> 1.4)
92 | nokogiri (1.16.7-x86_64-darwin)
93 | racc (~> 1.4)
94 | nokogiri (1.16.7-x86_64-linux)
95 | racc (~> 1.4)
96 | oj (3.16.7)
97 | bigdecimal (>= 3.0)
98 | ostruct (>= 0.2)
99 | ostruct (0.6.0)
100 | parallel (1.26.3)
101 | parser (3.3.5.1)
102 | ast (~> 2.4.1)
103 | racc
104 | pg (1.5.9)
105 | racc (1.8.1)
106 | rack (3.1.8)
107 | rack-session (2.0.0)
108 | rack (>= 3.0.0)
109 | rack-test (2.1.0)
110 | rack (>= 1.3)
111 | rails-dom-testing (2.2.0)
112 | activesupport (>= 5.0.0)
113 | minitest
114 | nokogiri (>= 1.6)
115 | rails-html-sanitizer (1.6.0)
116 | loofah (~> 2.21)
117 | nokogiri (~> 1.14)
118 | rainbow (3.1.1)
119 | rake (13.2.1)
120 | rake-compiler (1.2.8)
121 | rake
122 | regexp_parser (2.9.2)
123 | rspec (3.13.0)
124 | rspec-core (~> 3.13.0)
125 | rspec-expectations (~> 3.13.0)
126 | rspec-mocks (~> 3.13.0)
127 | rspec-core (3.13.2)
128 | rspec-support (~> 3.13.0)
129 | rspec-expectations (3.13.3)
130 | diff-lcs (>= 1.2.0, < 2.0)
131 | rspec-support (~> 3.13.0)
132 | rspec-mocks (3.13.2)
133 | diff-lcs (>= 1.2.0, < 2.0)
134 | rspec-support (~> 3.13.0)
135 | rspec-support (3.13.1)
136 | rubocop (1.66.1)
137 | json (~> 2.3)
138 | language_server-protocol (>= 3.17.0)
139 | parallel (~> 1.10)
140 | parser (>= 3.3.0.2)
141 | rainbow (>= 2.2.2, < 4.0)
142 | regexp_parser (>= 2.4, < 3.0)
143 | rubocop-ast (>= 1.32.2, < 2.0)
144 | ruby-progressbar (~> 1.7)
145 | unicode-display_width (>= 2.4.0, < 3.0)
146 | rubocop-ast (1.33.0)
147 | parser (>= 3.3.1.0)
148 | rubocop-performance (1.22.1)
149 | rubocop (>= 1.48.1, < 2.0)
150 | rubocop-ast (>= 1.31.1, < 2.0)
151 | rubocop-rspec (3.2.0)
152 | rubocop (~> 1.61)
153 | ruby-progressbar (1.13.0)
154 | securerandom (0.4.0)
155 | sqlite3 (2.2.0-aarch64-linux-gnu)
156 | sqlite3 (2.2.0-arm-linux-gnu)
157 | sqlite3 (2.2.0-arm64-darwin)
158 | sqlite3 (2.2.0-x86-linux-gnu)
159 | sqlite3 (2.2.0-x86_64-darwin)
160 | sqlite3 (2.2.0-x86_64-linux-gnu)
161 | stackprof (0.2.26)
162 | standard (1.41.1)
163 | language_server-protocol (~> 3.17.0.2)
164 | lint_roller (~> 1.0)
165 | rubocop (~> 1.66.0)
166 | standard-custom (~> 1.0.0)
167 | standard-performance (~> 1.5)
168 | standard-custom (1.0.2)
169 | lint_roller (~> 1.0)
170 | rubocop (~> 1.50)
171 | standard-performance (1.5.0)
172 | lint_roller (~> 1.1)
173 | rubocop-performance (~> 1.22.0)
174 | terminal-table (3.0.2)
175 | unicode-display_width (>= 1.1.1, < 3)
176 | thor (1.3.2)
177 | timeout (0.4.2)
178 | tzinfo (2.0.6)
179 | concurrent-ruby (~> 1.0)
180 | unicode-display_width (2.6.0)
181 | uri (1.0.2)
182 | useragent (0.16.10)
183 | vernier (1.3.0)
184 |
185 | PLATFORMS
186 | aarch64-linux
187 | arm-linux
188 | arm64-darwin
189 | x86-linux
190 | x86_64-darwin
191 | x86_64-linux
192 |
193 | DEPENDENCIES
194 | active_model_serializers (~> 0.10)
195 | activemodel (~> 8.0.0)
196 | activerecord (~> 8.0.0)
197 | activesupport (~> 8.0.0)
198 | appraisal
199 | benchmark-ips
200 | byebug
201 | faker
202 | memory_profiler
203 | panko_serializer!
204 | pg
205 | rake
206 | rake-compiler
207 | rspec (~> 3.0)
208 | rubocop
209 | rubocop-performance
210 | rubocop-rspec
211 | sqlite3 (>= 2.1)
212 | stackprof
213 | standard
214 | standard-performance
215 | terminal-table
216 | vernier
217 |
218 | BUNDLED WITH
219 | 2.5.21
220 |
--------------------------------------------------------------------------------
/lib/panko/array_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Panko
4 | class ArraySerializer
5 | attr_accessor :subjects
6 |
7 | def initialize(subjects, options = {})
8 | @subjects = subjects
9 | @each_serializer = options[:each_serializer]
10 |
11 | if @each_serializer.nil?
12 | raise ArgumentError, %{
13 | Please pass valid each_serializer to ArraySerializer, for example:
14 | > Panko::ArraySerializer.new(posts, each_serializer: PostSerializer)
15 | }
16 | end
17 |
18 | serializer_options = {
19 | only: options.fetch(:only, []),
20 | except: options.fetch(:except, []),
21 | context: options[:context],
22 | scope: options[:scope]
23 | }
24 |
25 | @serialization_context = SerializationContext.create(options)
26 | @descriptor = Panko::SerializationDescriptor.build(@each_serializer, serializer_options, @serialization_context)
27 | end
28 |
29 | def to_json
30 | serialize_to_json @subjects
31 | end
32 |
33 | def serialize(subjects)
34 | serialize_with_writer(subjects, Panko::ObjectWriter.new).output
35 | end
36 |
37 | def to_a
38 | serialize_with_writer(@subjects, Panko::ObjectWriter.new).output
39 | end
40 |
41 | def serialize_to_json(subjects)
42 | serialize_with_writer(subjects, Oj::StringWriter.new(mode: :rails)).to_s
43 | end
44 |
45 | private
46 |
47 | def serialize_with_writer(subjects, writer)
48 | Panko.serialize_objects(subjects.to_a, writer, @descriptor)
49 | writer
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/panko/association.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Panko
4 | class Association
5 | def duplicate
6 | Panko::Association.new(
7 | name_sym,
8 | name_str,
9 | Panko::SerializationDescriptor.duplicate(descriptor)
10 | )
11 | end
12 |
13 | def inspect
14 | ""
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/panko/attribute.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Panko
4 | class Attribute
5 | def self.create(name, alias_name: nil)
6 | alias_name = alias_name.to_s unless alias_name.nil?
7 | Attribute.new(name.to_s, alias_name)
8 | end
9 |
10 | def ==(other)
11 | return name.to_sym == other if other.is_a? Symbol
12 | return name == other.name && alias_name == other.alias_name if other.is_a? Panko::Attribute
13 |
14 | super
15 | end
16 |
17 | def hash
18 | name.to_sym.hash
19 | end
20 |
21 | def eql?(other)
22 | self.==(other)
23 | end
24 |
25 | def inspect
26 | ""
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/panko/object_writer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Panko::ObjectWriter
4 | def initialize
5 | @values = []
6 | @keys = []
7 |
8 | @next_key = nil
9 | @output = nil
10 | end
11 |
12 | def push_object(key = nil)
13 | @values << {}
14 | @keys << key
15 | end
16 |
17 | def push_array(key = nil)
18 | @values << []
19 | @keys << key
20 | end
21 |
22 | def push_key(key)
23 | @next_key = key
24 | end
25 |
26 | def push_value(value, key = nil)
27 | unless @next_key.nil?
28 | raise "push_value is called with key after push_key is called" unless key.nil?
29 | key = @next_key
30 | @next_key = nil
31 | end
32 |
33 | @values.last[key] = value.as_json
34 | end
35 |
36 | def push_json(value, key = nil)
37 | if value.is_a?(String)
38 | value = begin
39 | Oj.load(value)
40 | rescue
41 | nil
42 | end
43 | end
44 |
45 | push_value(value, key)
46 | end
47 |
48 | def pop
49 | result = @values.pop
50 |
51 | if @values.empty?
52 | @output = result
53 | return
54 | end
55 |
56 | scope_key = @keys.pop
57 | if scope_key.nil?
58 | @values.last << result
59 | else
60 | @values.last[scope_key] = result
61 | end
62 | end
63 |
64 | def output
65 | raise "Output is called before poping all" unless @values.empty?
66 | @output
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/panko/response.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "oj"
4 |
5 | module Panko
6 | JsonValue = Struct.new(:value) do
7 | def self.from(value)
8 | JsonValue.new(value)
9 | end
10 |
11 | def to_json
12 | value
13 | end
14 | end
15 |
16 | class ResponseCreator
17 | def self.value(value)
18 | Panko::Response.new(value)
19 | end
20 |
21 | def self.json(value)
22 | Panko::JsonValue.from(value)
23 | end
24 |
25 | def self.array_serializer(data, serializer, options = {})
26 | merged_options = options.merge(each_serializer: serializer)
27 | Panko::ArraySerializer.new(data, merged_options)
28 | end
29 |
30 | def self.serializer(data, serializer, options = {})
31 | json serializer.new(options).serialize_to_json(data)
32 | end
33 | end
34 |
35 | class Response
36 | def initialize(data)
37 | @data = data
38 | end
39 |
40 | def to_json(_options = nil)
41 | writer = Oj::StringWriter.new(mode: :rails)
42 | write(writer, @data)
43 | writer.to_s
44 | end
45 |
46 | def self.create
47 | Response.new(yield ResponseCreator)
48 | end
49 |
50 | private
51 |
52 | def write(writer, data, key = nil)
53 | return write_array(writer, data, key) if data.is_a?(Array)
54 |
55 | return write_object(writer, data, key) if data.is_a?(Hash)
56 |
57 | write_value(writer, data, key)
58 | end
59 |
60 | def write_array(writer, value, key = nil)
61 | writer.push_array key
62 | value.each { |v| write(writer, v) }
63 | writer.pop
64 | end
65 |
66 | def write_object(writer, value, key = nil)
67 | writer.push_object key
68 |
69 | value.each do |entry_key, entry_value|
70 | write(writer, entry_value, entry_key.to_s)
71 | end
72 |
73 | writer.pop
74 | end
75 |
76 | def write_value(writer, value, key = nil)
77 | if value.is_a?(Panko::ArraySerializer) ||
78 | value.is_a?(Panko::Serializer) ||
79 | value.is_a?(Panko::Response) ||
80 | value.is_a?(Panko::JsonValue)
81 | writer.push_json(value.to_json, key)
82 | else
83 | writer.push_value(value, key)
84 | end
85 | end
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/lib/panko/serialization_descriptor.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Panko
4 | class SerializationDescriptor
5 | #
6 | # Creates new description and apply the options
7 | # on the new descriptor
8 | #
9 | def self.build(serializer, options = {}, serialization_context = nil)
10 | backend = Panko::SerializationDescriptor.duplicate(serializer._descriptor)
11 |
12 | options.merge! serializer.filters_for(options[:context], options[:scope]) if serializer.respond_to? :filters_for
13 |
14 | backend.apply_filters(options)
15 |
16 | backend.set_serialization_context(serialization_context)
17 |
18 | backend
19 | end
20 |
21 | #
22 | # Create new descriptor with same properties
23 | # useful when you want to apply filters
24 | #
25 | def self.duplicate(descriptor)
26 | backend = Panko::SerializationDescriptor.new
27 |
28 | backend.type = descriptor.type
29 |
30 | backend.attributes = descriptor.attributes.dup
31 |
32 | backend.method_fields = descriptor.method_fields.dup
33 | backend.serializer = descriptor.type.new(_skip_init: true) unless backend.method_fields.empty?
34 |
35 | backend.has_many_associations = descriptor.has_many_associations.map(&:duplicate)
36 | backend.has_one_associations = descriptor.has_one_associations.map(&:duplicate)
37 |
38 | backend
39 | end
40 |
41 | def set_serialization_context(context)
42 | serializer.serialization_context = context if !method_fields.empty? && !serializer.nil?
43 |
44 | has_many_associations.each do |assoc|
45 | assoc.descriptor.set_serialization_context context
46 | end
47 |
48 | has_one_associations.each do |assoc|
49 | assoc.descriptor.set_serialization_context context
50 | end
51 | end
52 |
53 | #
54 | # Applies attributes and association filters
55 | #
56 | def apply_filters(options)
57 | return unless options.key?(:only) || options.key?(:except)
58 |
59 | attributes_only_filters, associations_only_filters = resolve_filters(options, :only)
60 | attributes_except_filters, associations_except_filters = resolve_filters(options, :except)
61 |
62 | self.attributes = apply_attribute_filters(
63 | attributes,
64 | attributes_only_filters,
65 | attributes_except_filters
66 | )
67 |
68 | self.method_fields = apply_attribute_filters(
69 | method_fields,
70 | attributes_only_filters,
71 | attributes_except_filters
72 | )
73 |
74 | unless has_many_associations.empty?
75 | self.has_many_associations = apply_association_filters(
76 | has_many_associations,
77 | {attributes: attributes_only_filters, associations: associations_only_filters},
78 | attributes: attributes_except_filters, associations: associations_except_filters
79 | )
80 | end
81 |
82 | unless has_one_associations.empty?
83 | self.has_one_associations = apply_association_filters(
84 | has_one_associations,
85 | {attributes: attributes_only_filters, associations: associations_only_filters},
86 | attributes: attributes_except_filters, associations: associations_except_filters
87 | )
88 | end
89 | end
90 |
91 | def apply_association_filters(associations, only_filters, except_filters)
92 | attributes_only_filters = only_filters[:attributes] || []
93 | unless attributes_only_filters.empty?
94 | associations.select! do |association|
95 | attributes_only_filters.include?(association.name_sym)
96 | end
97 | end
98 |
99 | attributes_except_filters = except_filters[:attributes] || []
100 | unless attributes_except_filters.empty?
101 | associations.reject! do |association|
102 | attributes_except_filters.include?(association.name_sym)
103 | end
104 | end
105 |
106 | associations_only_filters = only_filters[:associations]
107 | associations_except_filters = except_filters[:associations]
108 |
109 | return associations if associations_only_filters.empty? && associations_except_filters.empty?
110 |
111 | associations.map do |association|
112 | name = association.name_sym
113 | descriptor = association.descriptor
114 |
115 | only_filter = associations_only_filters[name]
116 | except_filter = associations_except_filters[name]
117 |
118 | filters = {}
119 | filters[:only] = only_filter unless only_filter.nil?
120 | filters[:except] = except_filter unless except_filter.nil?
121 |
122 | unless filters.empty?
123 | next Panko::Association.new(
124 | name,
125 | association.name_str,
126 | Panko::SerializationDescriptor.build(descriptor.type, filters)
127 | )
128 | end
129 |
130 | association
131 | end
132 | end
133 |
134 | def resolve_filters(options, filter)
135 | filters = options.fetch(filter, {})
136 | return filters, {} if filters.is_a? Array
137 |
138 | # hash filters looks like this
139 | # { instance: [:a], foo: [:b] }
140 | # which mean, for the current instance use `[:a]` as filter
141 | # and for association named `foo` use `[:b]`
142 |
143 | return [], {} if filters.empty?
144 |
145 | attributes_filters = filters.fetch(:instance, [])
146 | association_filters = filters.except(:instance)
147 |
148 | [attributes_filters, association_filters]
149 | end
150 |
151 | def apply_fields_filters(fields, only, except)
152 | return fields & only unless only.empty?
153 | return fields - except unless except.empty?
154 |
155 | fields
156 | end
157 |
158 | def apply_attribute_filters(attributes, only, except)
159 | unless only.empty?
160 | attributes = attributes.select do |attribute|
161 | name_to_check = attribute.name
162 | name_to_check = attribute.alias_name unless attribute.alias_name.nil?
163 |
164 | only.include?(name_to_check.to_sym)
165 | end
166 | end
167 |
168 | unless except.empty?
169 | attributes = attributes.reject do |attribute|
170 | name_to_check = attribute.name
171 | name_to_check = attribute.alias_name unless attribute.alias_name.nil?
172 |
173 | except.include?(name_to_check.to_sym)
174 | end
175 | end
176 |
177 | attributes
178 | end
179 | end
180 | end
181 |
--------------------------------------------------------------------------------
/lib/panko/serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "serialization_descriptor"
4 | require "oj"
5 |
6 | class SerializationContext
7 | attr_accessor :context, :scope
8 |
9 | def initialize(context, scope)
10 | @context = context
11 | @scope = scope
12 | end
13 |
14 | def self.create(options)
15 | if options.key?(:context) || options.key?(:scope)
16 | SerializationContext.new(options[:context], options[:scope])
17 | else
18 | EmptySerializerContext.new
19 | end
20 | end
21 | end
22 |
23 | class EmptySerializerContext
24 | def scope
25 | nil
26 | end
27 |
28 | def context
29 | nil
30 | end
31 | end
32 |
33 | module Panko
34 | class Serializer
35 | SKIP = Object.new.freeze
36 |
37 | class << self
38 | def inherited(base)
39 | if _descriptor.nil?
40 | base._descriptor = Panko::SerializationDescriptor.new
41 |
42 | base._descriptor.attributes = []
43 | base._descriptor.aliases = {}
44 |
45 | base._descriptor.method_fields = []
46 |
47 | base._descriptor.has_many_associations = []
48 | base._descriptor.has_one_associations = []
49 | else
50 | base._descriptor = Panko::SerializationDescriptor.duplicate(_descriptor)
51 | end
52 | base._descriptor.type = base
53 | end
54 |
55 | attr_accessor :_descriptor
56 |
57 | def attributes(*attrs)
58 | @_descriptor.attributes.push(*attrs.map { |attr| Attribute.create(attr) }).uniq!
59 | end
60 |
61 | def aliases(aliases = {})
62 | aliases.each do |attr, alias_name|
63 | @_descriptor.attributes << Attribute.create(attr, alias_name: alias_name)
64 | end
65 | end
66 |
67 | def method_added(method)
68 | super
69 |
70 | return if @_descriptor.nil?
71 |
72 | deleted_attr = @_descriptor.attributes.delete(method)
73 | @_descriptor.method_fields << Attribute.create(deleted_attr.name, alias_name: deleted_attr.alias_name) unless deleted_attr.nil?
74 | end
75 |
76 | def has_one(name, options = {})
77 | serializer_const = options[:serializer]
78 | if serializer_const.is_a?(String)
79 | serializer_const = Panko::SerializerResolver.resolve(serializer_const, self)
80 | end
81 | serializer_const ||= Panko::SerializerResolver.resolve(name.to_s, self)
82 |
83 | raise "Can't find serializer for #{self.name}.#{name} has_one relationship." if serializer_const.nil?
84 |
85 | @_descriptor.has_one_associations << Panko::Association.new(
86 | name,
87 | options.fetch(:name, name).to_s,
88 | Panko::SerializationDescriptor.build(serializer_const, options)
89 | )
90 | end
91 |
92 | def has_many(name, options = {})
93 | serializer_const = options[:serializer] || options[:each_serializer]
94 | if serializer_const.is_a?(String)
95 | serializer_const = Panko::SerializerResolver.resolve(serializer_const, self)
96 | end
97 | serializer_const ||= Panko::SerializerResolver.resolve(name.to_s, self)
98 |
99 | raise "Can't find serializer for #{self.name}.#{name} has_many relationship." if serializer_const.nil?
100 |
101 | @_descriptor.has_many_associations << Panko::Association.new(
102 | name,
103 | options.fetch(:name, name).to_s,
104 | Panko::SerializationDescriptor.build(serializer_const, options)
105 | )
106 | end
107 | end
108 |
109 | def initialize(options = {})
110 | # this "_skip_init" trick is so I can create serializers from serialization descriptor
111 | return if options[:_skip_init]
112 |
113 | @serialization_context = SerializationContext.create(options)
114 | @descriptor = Panko::SerializationDescriptor.build(self.class, options, @serialization_context)
115 | @used = false
116 | end
117 |
118 | def context
119 | @serialization_context.context
120 | end
121 |
122 | def scope
123 | @serialization_context.scope
124 | end
125 |
126 | attr_writer :serialization_context
127 | attr_reader :object
128 |
129 | def serialize(object)
130 | serialize_with_writer(object, Panko::ObjectWriter.new).output
131 | end
132 |
133 | def serialize_to_json(object)
134 | serialize_with_writer(object, Oj::StringWriter.new(mode: :rails)).to_s
135 | end
136 |
137 | private
138 |
139 | def serialize_with_writer(object, writer)
140 | raise ArgumentError.new("Panko::Serializer instances are single-use") if @used
141 | Panko.serialize_object(object, writer, @descriptor)
142 | @used = true
143 | writer
144 | end
145 | end
146 | end
147 |
--------------------------------------------------------------------------------
/lib/panko/serializer_resolver.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "active_support/core_ext/string/inflections"
4 | require "active_support/core_ext/module/introspection"
5 |
6 | class Panko::SerializerResolver
7 | class << self
8 | def resolve(name, from)
9 | serializer_const = nil
10 |
11 | namespace = namespace_for(from)
12 |
13 | if namespace.present?
14 | serializer_const = safe_serializer_get("#{namespace}::#{name.singularize.camelize}Serializer")
15 | end
16 |
17 | serializer_const ||= safe_serializer_get("#{name.singularize.camelize}Serializer")
18 | serializer_const ||= safe_serializer_get(name)
19 | serializer_const
20 | end
21 |
22 | private
23 |
24 | if Module.method_defined?(:module_parent_name)
25 | def namespace_for(from)
26 | from.module_parent_name
27 | end
28 | else
29 | def namespace_for(from)
30 | from.parent_name
31 | end
32 | end
33 |
34 | def safe_serializer_get(name)
35 | const = Object.const_get(name)
36 | (const < Panko::Serializer) ? const : nil
37 | rescue NameError
38 | nil
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/panko/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Panko
4 | VERSION = "0.8.3"
5 | end
6 |
--------------------------------------------------------------------------------
/lib/panko_serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "panko/version"
4 | require "panko/attribute"
5 | require "panko/association"
6 | require "panko/serializer"
7 | require "panko/array_serializer"
8 | require "panko/response"
9 | require "panko/serializer_resolver"
10 | require "panko/object_writer"
11 |
12 | # C Extension
13 | require "oj"
14 | require "panko/panko_serializer"
15 |
--------------------------------------------------------------------------------
/panko_serializer.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | lib = File.expand_path("lib", __dir__)
4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5 | require "panko/version"
6 |
7 | Gem::Specification.new do |spec|
8 | spec.name = "panko_serializer"
9 | spec.version = Panko::VERSION
10 | spec.authors = ["Yosi Attias"]
11 | spec.email = ["yosy101@gmail.com"]
12 |
13 | spec.summary = "High Performance JSON Serialization for ActiveRecord & Ruby Objects"
14 | spec.homepage = "https://panko.dev"
15 | spec.license = "MIT"
16 |
17 | spec.metadata = {
18 | "bug_tracker_uri" => "https://github.com/yosiat/panko_serializer/issues",
19 | "source_code_uri" => "https://github.com/yosiat/panko_serializer",
20 | "documentation_uri" => "https://panko.dev",
21 | "changelog_uri" => "https://github.com/yosiat/panko_serializer/releases"
22 | }
23 |
24 | spec.required_ruby_version = ">= 3.1.0"
25 |
26 | spec.files = `git ls-files -z`.split("\x0").reject do |f|
27 | f.match(%r{^(test|spec|features)/})
28 | end
29 | spec.require_paths = ["lib"]
30 |
31 | spec.extensions << "ext/panko_serializer/extconf.rb"
32 |
33 | spec.add_dependency "oj", "> 3.11.0", "< 4.0.0"
34 | spec.add_dependency "activesupport"
35 | spec.add_development_dependency "appraisal"
36 | end
37 |
--------------------------------------------------------------------------------
/spec/models.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "active_record"
4 | require "sqlite3"
5 |
6 | # Change the following to reflect your database settings
7 | ActiveRecord::Base.establish_connection(
8 | adapter: "sqlite3",
9 | database: ":memory:"
10 | )
11 |
12 | # Don't show migration output when constructing fake db
13 | ActiveRecord::Migration.verbose = false
14 |
15 | ActiveRecord::Schema.define do
16 | create_table :foos, force: true do |t|
17 | t.string :name
18 | t.string :address
19 |
20 | t.references :foos_holder
21 | t.references :foo_holder
22 |
23 | t.timestamps(null: false)
24 | end
25 |
26 | create_table :goos, force: true do |t|
27 | t.string :name
28 | t.string :address
29 |
30 | t.references :foos_holder
31 | t.references :foo_holder
32 |
33 | t.timestamps(null: false)
34 | end
35 |
36 | create_table :foo_holders, force: true do |t|
37 | t.string :name
38 | t.references :foo
39 |
40 | t.timestamps(null: false)
41 | end
42 |
43 | create_table :foos_holders, force: true do |t|
44 | t.string :name
45 |
46 | t.timestamps(null: false)
47 | end
48 | end
49 |
50 | class Foo < ActiveRecord::Base
51 | end
52 |
53 | class Goo < ActiveRecord::Base
54 | end
55 |
56 | class FoosHolder < ActiveRecord::Base
57 | has_many :foos
58 | has_many :goos
59 | end
60 |
61 | class FooHolder < ActiveRecord::Base
62 | has_one :foo
63 | has_one :goo
64 | end
65 |
--------------------------------------------------------------------------------
/spec/panko/array_serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe Panko::ArraySerializer do
6 | class FooSerializer < Panko::Serializer
7 | attributes :name, :address
8 | end
9 |
10 | it "throws argument error when each_serializer isnt passed" do
11 | expect do
12 | Panko::ArraySerializer.new([])
13 | end.to raise_error(ArgumentError)
14 | end
15 |
16 | context "sanity" do
17 | it "serializers array of elements" do
18 | array_serializer_factory = -> { Panko::ArraySerializer.new([], each_serializer: FooSerializer) }
19 |
20 | foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
21 | foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
22 |
23 | expect(Foo.all).to serialized_as(array_serializer_factory, [
24 | {"name" => foo1.name, "address" => foo1.address},
25 | {"name" => foo2.name, "address" => foo2.address}
26 | ])
27 | end
28 |
29 | it "serializes array of elements with virtual attribtues" do
30 | class TestSerializerWithMethodsSerializer < Panko::Serializer
31 | attributes :name, :address, :something, :context_fetch
32 |
33 | def something
34 | "#{object.name} #{object.address}"
35 | end
36 |
37 | def context_fetch
38 | context[:value]
39 | end
40 | end
41 |
42 | foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word).reload
43 |
44 | array_serializer_factory = -> {
45 | Panko::ArraySerializer.new([],
46 | each_serializer: TestSerializerWithMethodsSerializer,
47 | context: {value: 6})
48 | }
49 |
50 | expect(Foo.all).to serialized_as(array_serializer_factory, [{"name" => foo.name,
51 | "address" => foo.address,
52 | "something" => "#{foo.name} #{foo.address}",
53 | "context_fetch" => 6}])
54 | end
55 | end
56 |
57 | context "filter" do
58 | it "only" do
59 | array_serializer_factory = -> { Panko::ArraySerializer.new([], each_serializer: FooSerializer, only: [:name]) }
60 |
61 | foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
62 | foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
63 |
64 | expect(Foo.all).to serialized_as(array_serializer_factory, [
65 | {"name" => foo1.name},
66 | {"name" => foo2.name}
67 | ])
68 | end
69 |
70 | it "except" do
71 | array_serializer_factory = -> { Panko::ArraySerializer.new([], each_serializer: FooSerializer, except: [:name]) }
72 |
73 | foo1 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
74 | foo2 = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
75 |
76 | expect(Foo.all).to serialized_as(array_serializer_factory, [
77 | {"address" => foo1.address},
78 | {"address" => foo2.address}
79 | ])
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/spec/panko/object_writer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "active_record/connection_adapters/postgresql_adapter"
5 |
6 | describe Panko::ObjectWriter do
7 | let(:writer) { Panko::ObjectWriter.new }
8 |
9 | context "push_object" do
10 | it "property" do
11 | writer.push_object
12 | writer.push_value "yosi", "name"
13 | writer.pop
14 |
15 | expect(writer.output).to eql("name" => "yosi")
16 | end
17 |
18 | it "property with separate key value instructions" do
19 | writer.push_object
20 | writer.push_key "name"
21 | writer.push_value "yosi"
22 | writer.pop
23 |
24 | expect(writer.output).to eql("name" => "yosi")
25 | end
26 |
27 | it "supports nested objects" do
28 | writer.push_object
29 | writer.push_value "yosi", "name"
30 |
31 | writer.push_object("nested")
32 | writer.push_value "key1", "value"
33 | writer.pop
34 |
35 | writer.pop
36 |
37 | expect(writer.output).to eql(
38 | "name" => "yosi",
39 | "nested" => {
40 | "value" => "key1"
41 | }
42 | )
43 | end
44 |
45 | it "supports nested arrays" do
46 | writer.push_object
47 | writer.push_value "yosi", "name"
48 |
49 | writer.push_object("nested")
50 | writer.push_value "key1", "value"
51 | writer.pop
52 |
53 | writer.push_array "values"
54 | writer.push_object
55 | writer.push_value "item", "key"
56 | writer.pop
57 | writer.pop
58 |
59 | writer.pop
60 |
61 | expect(writer.output).to eql(
62 | "name" => "yosi",
63 | "nested" => {
64 | "value" => "key1"
65 | },
66 | "values" => [
67 | {"key" => "item"}
68 | ]
69 | )
70 | end
71 | end
72 |
73 | it "supports arrays" do
74 | writer.push_array
75 |
76 | writer.push_object
77 | writer.push_value "key1", "value"
78 | writer.pop
79 |
80 | writer.push_object
81 | writer.push_value "key2", "value2"
82 | writer.pop
83 |
84 | writer.pop
85 |
86 | expect(writer.output).to eql([
87 | {
88 | "value" => "key1"
89 | },
90 | {
91 | "value2" => "key2"
92 | }
93 | ])
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/spec/panko/response_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe Panko::Response do
6 | class FooSerializer < Panko::Serializer
7 | attributes :name, :address
8 | end
9 |
10 | class FooWithContextSerializer < Panko::Serializer
11 | attributes :name, :context_value
12 |
13 | def context_value
14 | context[:value]
15 | end
16 | end
17 |
18 | it "serializes primitive values" do
19 | response = Panko::Response.new(success: true, num: 1)
20 |
21 | json_response = Oj.load(response.to_json)
22 |
23 | expect(json_response["success"]).to eq(true)
24 | expect(json_response["num"]).to eq(1)
25 | end
26 |
27 | it "serializes hash values" do
28 | hash = {"a" => 1, "b" => 2}
29 | response = Panko::Response.new(success: true, hash: hash)
30 |
31 | json_response = Oj.load(response.to_json)
32 |
33 | expect(json_response["hash"]).to eq(hash)
34 | end
35 |
36 | it "serializes json wrapped in json value" do
37 | response = Panko::Response.new(success: true, value: Panko::JsonValue.from('{"a":1}'))
38 |
39 | json_response = Oj.load(response.to_json)
40 |
41 | expect(json_response["success"]).to eq(true)
42 | expect(json_response["value"]).to eq("a" => 1)
43 | end
44 |
45 | it "serializes array serializer" do
46 | foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
47 |
48 | response = Panko::Response.new(success: true,
49 | foos: Panko::ArraySerializer.new(Foo.all, each_serializer: FooSerializer))
50 |
51 | json_response = Oj.load(response.to_json)
52 |
53 | expect(json_response["success"]).to eq(true)
54 | expect(json_response["foos"]).to eq([
55 | "name" => foo.name,
56 | "address" => foo.address
57 | ])
58 | end
59 |
60 | it "supports nesting of responses" do
61 | foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
62 |
63 | response = Panko::Response.new(
64 | data: Panko::Response.new(
65 | data: Panko::Response.new(
66 | rows: [
67 | Panko::Response.new(
68 | foos: Panko::ArraySerializer.new(Foo.all, each_serializer: FooSerializer)
69 | )
70 | ]
71 | )
72 | )
73 | )
74 |
75 | json_response = Oj.load(response.to_json)
76 |
77 | expect(json_response).to eq(
78 | "data" => {
79 | "data" => {
80 | "rows" => [
81 | "foos" => [{
82 | "name" => foo.name,
83 | "address" => foo.address
84 | }]
85 | ]
86 | }
87 | }
88 | )
89 | end
90 |
91 | it "supports array" do
92 | response = Panko::Response.new([
93 | data: Panko::Response.new(
94 | json_data: Panko::JsonValue.from({a: 1}.to_json)
95 | )
96 | ])
97 |
98 | json_response = Oj.load(response.to_json)
99 |
100 | expect(json_response).to eql([
101 | {"data" => {"json_data" => {"a" => 1}}}
102 | ])
103 | end
104 |
105 | it "create" do
106 | foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
107 |
108 | response = Panko::Response.create do |t|
109 | [
110 | {
111 | data: t.value(
112 | json_data: t.json({a: 1}.to_json),
113 | foos: t.array_serializer(Foo.all, FooSerializer),
114 | foo: t.serializer(Foo.first, FooSerializer)
115 | )
116 | }
117 | ]
118 | end
119 |
120 | json_response = Oj.load(response.to_json)
121 |
122 | expect(json_response).to eql([
123 | {"data" =>
124 | {
125 | "json_data" => {"a" => 1},
126 | "foo" => {
127 | "name" => foo.name,
128 | "address" => foo.address
129 | },
130 | "foos" => [{
131 | "name" => foo.name,
132 | "address" => foo.address
133 | }]
134 | }}
135 | ])
136 | end
137 |
138 | it "create with context" do
139 | foo = Foo.create(name: Faker::Lorem.word, address: Faker::Lorem.word)
140 | context = {value: Faker::Lorem.word}
141 |
142 | response = Panko::Response.create do |t|
143 | [
144 | {
145 | data: t.value(
146 | foos: t.array_serializer(Foo.all, FooWithContextSerializer, context: context),
147 | foo: t.serializer(Foo.first, FooWithContextSerializer, context: context)
148 | )
149 | }
150 | ]
151 | end
152 |
153 | json_response = Oj.load(response.to_json)
154 |
155 | expect(json_response).to eql([
156 | {"data" =>
157 | {
158 | "foo" => {
159 | "name" => foo.name,
160 | "context_value" => context[:value]
161 | },
162 | "foos" => [{
163 | "name" => foo.name,
164 | "context_value" => context[:value]
165 | }]
166 | }}
167 | ])
168 | end
169 | end
170 |
--------------------------------------------------------------------------------
/spec/panko/serialization_descriptor_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe Panko::SerializationDescriptor do
6 | class FooSerializer < Panko::Serializer
7 | attributes :name, :address
8 | end
9 |
10 | context "attributes" do
11 | it "simple fields" do
12 | descriptor = Panko::SerializationDescriptor.build(FooSerializer)
13 |
14 | expect(descriptor).not_to be_nil
15 | expect(descriptor.attributes).to eq([
16 | Panko::Attribute.create(:name),
17 | Panko::Attribute.create(:address)
18 | ])
19 | end
20 |
21 | it "method attributes" do
22 | class SerializerWithMethodsSerializer < Panko::Serializer
23 | attributes :name, :address, :something
24 |
25 | def something
26 | "#{object.name} #{object.address}"
27 | end
28 | end
29 |
30 | descriptor = Panko::SerializationDescriptor.build(SerializerWithMethodsSerializer)
31 |
32 | expect(descriptor).not_to be_nil
33 | expect(descriptor.attributes).to eq([
34 | Panko::Attribute.create(:name),
35 | Panko::Attribute.create(:address)
36 | ])
37 | expect(descriptor.method_fields).to eq([:something])
38 | end
39 |
40 | it "aliases" do
41 | class AttribteAliasesSerializer < Panko::Serializer
42 | aliases name: :full_name
43 | end
44 |
45 | descriptor = Panko::SerializationDescriptor.build(AttribteAliasesSerializer)
46 |
47 | expect(descriptor).not_to be_nil
48 | expect(descriptor.attributes).to eq([
49 | Panko::Attribute.create(:name, alias_name: :full_name)
50 | ])
51 | end
52 |
53 | it "allows multiple filters in other runs" do
54 | class MultipleFiltersTestSerializer < Panko::Serializer
55 | attributes :name, :address
56 | has_many :foos, each_serializer: FooSerializer
57 | end
58 |
59 | descriptor = Panko::SerializationDescriptor.build(MultipleFiltersTestSerializer, only: {
60 | instance: [:foos],
61 | foos: [:name]
62 | })
63 |
64 | expect(descriptor.has_many_associations.first.descriptor.attributes).to eq([
65 | Panko::Attribute.create(:name)
66 | ])
67 |
68 | descriptor = Panko::SerializationDescriptor.build(MultipleFiltersTestSerializer)
69 |
70 | expect(descriptor.has_many_associations.first.descriptor.attributes).to eq([
71 | Panko::Attribute.create(:name),
72 | Panko::Attribute.create(:address)
73 | ])
74 | end
75 | end
76 |
77 | context "associations" do
78 | it "has_one: build_descriptor" do
79 | class BuilderTestFooHolderHasOneSerializer < Panko::Serializer
80 | attributes :name
81 |
82 | has_one :foo, serializer: FooSerializer
83 | end
84 |
85 | descriptor = Panko::SerializationDescriptor.build(BuilderTestFooHolderHasOneSerializer)
86 |
87 | expect(descriptor).not_to be_nil
88 | expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)])
89 | expect(descriptor.method_fields).to be_empty
90 |
91 | expect(descriptor.has_one_associations.count).to eq(1)
92 |
93 | foo_association = descriptor.has_one_associations.first
94 | expect(foo_association.name_sym).to eq(:foo)
95 |
96 | foo_descriptor = Panko::SerializationDescriptor.build(FooSerializer, {})
97 | expect(foo_association.descriptor.attributes).to eq(foo_descriptor.attributes)
98 | expect(foo_association.descriptor.method_fields).to eq(foo_descriptor.method_fields)
99 | end
100 |
101 | it "has_many: builds descriptor" do
102 | class BuilderTestFoosHasManyHolderSerializer < Panko::Serializer
103 | attributes :name
104 |
105 | has_many :foos, serializer: FooSerializer
106 | end
107 |
108 | descriptor = Panko::SerializationDescriptor.build(BuilderTestFoosHasManyHolderSerializer)
109 |
110 | expect(descriptor).not_to be_nil
111 | expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)])
112 | expect(descriptor.method_fields).to be_empty
113 | expect(descriptor.has_one_associations).to be_empty
114 |
115 | expect(descriptor.has_many_associations.count).to eq(1)
116 |
117 | foo_association = descriptor.has_many_associations.first
118 | expect(foo_association.name_sym).to eq(:foos)
119 |
120 | foo_descriptor = Panko::SerializationDescriptor.build(FooSerializer, {})
121 | expect(foo_association.descriptor.attributes).to eq(foo_descriptor.attributes)
122 | expect(foo_association.descriptor.method_fields).to eq(foo_descriptor.method_fields)
123 | end
124 | end
125 |
126 | context "filter" do
127 | it "only" do
128 | descriptor = Panko::SerializationDescriptor.build(FooSerializer, only: [:name])
129 |
130 | expect(descriptor).not_to be_nil
131 | expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)])
132 | expect(descriptor.method_fields).to be_empty
133 | end
134 |
135 | it "except" do
136 | descriptor = Panko::SerializationDescriptor.build(FooSerializer, except: [:name])
137 |
138 | expect(descriptor).not_to be_nil
139 | expect(descriptor.attributes).to eq([Panko::Attribute.create(:address)])
140 | expect(descriptor.method_fields).to be_empty
141 | end
142 |
143 | it "except filters aliases" do
144 | class ExceptFooWithAliasesSerializer < Panko::Serializer
145 | aliases name: :full_name, address: :full_address
146 | end
147 |
148 | descriptor = Panko::SerializationDescriptor.build(ExceptFooWithAliasesSerializer, except: [:full_name])
149 |
150 | expect(descriptor).not_to be_nil
151 | expect(descriptor.attributes).to eq([Panko::Attribute.create(:address, alias_name: :full_address)])
152 | end
153 |
154 | it "only filters aliases" do
155 | class OnlyFooWithAliasesSerializer < Panko::Serializer
156 | attributes :address
157 | aliases name: :full_name
158 | end
159 |
160 | descriptor = Panko::SerializationDescriptor.build(OnlyFooWithAliasesSerializer, only: [:full_name])
161 |
162 | expect(descriptor).not_to be_nil
163 | expect(descriptor.attributes).to eq([Panko::Attribute.create(:name, alias_name: :full_name)])
164 | end
165 |
166 | it "only - filters aliases and fields" do
167 | class OnlyWithFieldsFooWithAliasesSerializer < Panko::Serializer
168 | attributes :address, :another_field
169 | aliases name: :full_name
170 | end
171 |
172 | descriptor = Panko::SerializationDescriptor.build(OnlyWithFieldsFooWithAliasesSerializer, only: %i[full_name address])
173 |
174 | expect(descriptor).not_to be_nil
175 | expect(descriptor.attributes).to eq([
176 | Panko::Attribute.create(:address),
177 | Panko::Attribute.create(:name, alias_name: :full_name)
178 | ])
179 | end
180 |
181 | it "filters associations" do
182 | class FooHasOneSerilizers < Panko::Serializer
183 | attributes :name
184 |
185 | has_one :foo1, serializer: FooSerializer
186 | has_one :foo2, serializer: FooSerializer
187 | end
188 |
189 | descriptor = Panko::SerializationDescriptor.build(FooHasOneSerilizers, only: %i[name foo1])
190 |
191 | expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)])
192 | expect(descriptor.has_one_associations.count).to eq(1)
193 |
194 | foos_assoc = descriptor.has_one_associations.first
195 | expect(foos_assoc.name_sym).to eq(:foo1)
196 | expect(foos_assoc.descriptor.attributes).to eq([Panko::Attribute.create(:name), Panko::Attribute.create(:address)])
197 | end
198 |
199 | describe "association filters" do
200 | it "accepts only as option" do
201 | class AssocFilterTestFoosHolderSerializer < Panko::Serializer
202 | attributes :name
203 | has_many :foos, serializer: FooSerializer
204 | end
205 |
206 | descriptor = Panko::SerializationDescriptor.build(AssocFilterTestFoosHolderSerializer, only: {foos: [:address]})
207 |
208 | expect(descriptor.attributes).to eq([Panko::Attribute.create(:name)])
209 | expect(descriptor.has_many_associations.count).to eq(1)
210 |
211 | foos_assoc = descriptor.has_many_associations.first
212 | expect(foos_assoc.name_sym).to eq(:foos)
213 | expect(foos_assoc.descriptor.attributes).to eq([Panko::Attribute.create(:address)])
214 | end
215 | end
216 | end
217 | end
218 |
--------------------------------------------------------------------------------
/spec/panko/serializer_resolver_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe Panko::SerializerResolver do
6 | it "resolves serializer on singular name" do
7 | class CoolSerializer < Panko::Serializer
8 | end
9 |
10 | result = Panko::SerializerResolver.resolve("cool", Object)
11 |
12 | expect(result._descriptor).to be_a(Panko::SerializationDescriptor)
13 | expect(result._descriptor.type).to eq(CoolSerializer)
14 | end
15 |
16 | it "resolves serializer on plural name" do
17 | class PersonSerializer < Panko::Serializer
18 | end
19 |
20 | result = Panko::SerializerResolver.resolve("persons", Object)
21 |
22 | expect(result._descriptor).to be_a(Panko::SerializationDescriptor)
23 | expect(result._descriptor.type).to eq(PersonSerializer)
24 | end
25 |
26 | it "resolves serializer on multiple-word name" do
27 | class MyCoolSerializer < Panko::Serializer
28 | end
29 |
30 | result = Panko::SerializerResolver.resolve("my_cool", Object)
31 |
32 | expect(result._descriptor).to be_a(Panko::SerializationDescriptor)
33 | expect(result._descriptor.type).to eq(MyCoolSerializer)
34 | end
35 |
36 | it "resolves serializer in namespace first" do
37 | class CoolSerializer < Panko::Serializer
38 | end
39 |
40 | module MyApp
41 | class CoolSerializer < Panko::Serializer
42 | end
43 |
44 | class PersonSerializer < Panko::Serializer
45 | end
46 | end
47 |
48 | result = Panko::SerializerResolver.resolve("cool", MyApp::PersonSerializer)
49 | expect(result._descriptor).to be_a(Panko::SerializationDescriptor)
50 | expect(result._descriptor.type).to eq(MyApp::CoolSerializer)
51 |
52 | result = Panko::SerializerResolver.resolve("cool", Panko)
53 | expect(result._descriptor).to be_a(Panko::SerializationDescriptor)
54 | expect(result._descriptor.type).to eq(CoolSerializer)
55 | end
56 |
57 | describe "errors cases" do
58 | it "returns nil when the serializer name can't be found" do
59 | expect(Panko::SerializerResolver.resolve("post", Object)).to be_nil
60 | end
61 |
62 | it "returns nil when the serializer is not Panko::Serializer" do
63 | class SomeObjectSerializer
64 | end
65 |
66 | expect(Panko::SerializerResolver.resolve("some_object", Object)).to be_nil
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/spec/panko/type_cast_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "active_record/connection_adapters/postgresql_adapter"
5 |
6 | def check_if_exists(module_name)
7 | mod = (begin
8 | module_name.constantize
9 | rescue
10 | nil
11 | end)
12 | return true if mod
13 | false unless mod
14 | end
15 |
16 | describe "Type Casting" do
17 | describe "String / Text" do
18 | context "ActiveRecord::Type::String" do
19 | let(:type) { ActiveRecord::Type::String.new }
20 |
21 | it { expect(Panko._type_cast(type, true)).to eq("t") }
22 | it { expect(Panko._type_cast(type, nil)).to be_nil }
23 | it { expect(Panko._type_cast(type, false)).to eq("f") }
24 | it { expect(Panko._type_cast(type, 123)).to eq("123") }
25 | it { expect(Panko._type_cast(type, "hello world")).to eq("hello world") }
26 | end
27 |
28 | context "ActiveRecord::Type::Text" do
29 | let(:type) { ActiveRecord::Type::Text.new }
30 |
31 | it { expect(Panko._type_cast(type, true)).to eq("t") }
32 | it { expect(Panko._type_cast(type, false)).to eq("f") }
33 | it { expect(Panko._type_cast(type, 123)).to eq("123") }
34 | it { expect(Panko._type_cast(type, "hello world")).to eq("hello world") }
35 | end
36 |
37 | # We treat uuid as stirng, there is no need for type cast before serialization
38 | context "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid" do
39 | let(:type) { ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Uuid.new }
40 |
41 | it { expect(Panko._type_cast(type, "e67d284b-87b8-445e-a20d-3c76ea353866")).to eq("e67d284b-87b8-445e-a20d-3c76ea353866") }
42 | end
43 | end
44 |
45 | describe "Integer" do
46 | context "ActiveRecord::Type::Integer" do
47 | let(:type) { ActiveRecord::Type::Integer.new }
48 |
49 | it { expect(Panko._type_cast(type, "")).to be_nil }
50 | it { expect(Panko._type_cast(type, nil)).to be_nil }
51 |
52 | it { expect(Panko._type_cast(type, 1)).to eq(1) }
53 | it { expect(Panko._type_cast(type, "1")).to eq(1) }
54 | it { expect(Panko._type_cast(type, 1.7)).to eq(1) }
55 |
56 | it { expect(Panko._type_cast(type, true)).to eq(1) }
57 | it { expect(Panko._type_cast(type, false)).to eq(0) }
58 |
59 | it { expect(Panko._type_cast(type, [6])).to be_nil }
60 | it { expect(Panko._type_cast(type, six: 6)).to be_nil }
61 | end
62 |
63 | context "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer", if: check_if_exists("ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer") do
64 | let(:type) { ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Integer.new }
65 |
66 | it { expect(Panko._type_cast(type, "")).to be_nil }
67 | it { expect(Panko._type_cast(type, nil)).to be_nil }
68 |
69 | it { expect(Panko._type_cast(type, 1)).to eq(1) }
70 | it { expect(Panko._type_cast(type, "1")).to eq(1) }
71 | it { expect(Panko._type_cast(type, 1.7)).to eq(1) }
72 |
73 | it { expect(Panko._type_cast(type, true)).to eq(1) }
74 | it { expect(Panko._type_cast(type, false)).to eq(0) }
75 |
76 | it { expect(Panko._type_cast(type, [6])).to be_nil }
77 | it { expect(Panko._type_cast(type, six: 6)).to be_nil }
78 | end
79 | end
80 |
81 | context "ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json", if: check_if_exists("ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json") do
82 | let(:type) { ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Json.new }
83 |
84 | it { expect(Panko._type_cast(type, "")).to be_nil }
85 | it { expect(Panko._type_cast(type, "shnitzel")).to be_nil }
86 | it { expect(Panko._type_cast(type, nil)).to be_nil }
87 |
88 | it { expect(Panko._type_cast(type, '{"a":1}')).to eq('{"a":1}') }
89 | it { expect(Panko._type_cast(type, "[6,12]")).to eq("[6,12]") }
90 |
91 | it { expect(Panko._type_cast(type, "a" => 1)).to eq("a" => 1) }
92 | it { expect(Panko._type_cast(type, [6, 12])).to eq([6, 12]) }
93 | end
94 |
95 | context "ActiveRecord::Type::Json", if: check_if_exists("ActiveRecord::Type::Json") do
96 | let(:type) { ActiveRecord::Type::Json.new }
97 |
98 | it { expect(Panko._type_cast(type, "")).to be_nil }
99 | it { expect(Panko._type_cast(type, "shnitzel")).to be_nil }
100 | it { expect(Panko._type_cast(type, nil)).to be_nil }
101 |
102 | it { expect(Panko._type_cast(type, '{"a":1}')).to eq('{"a":1}') }
103 | it { expect(Panko._type_cast(type, "[6,12]")).to eq("[6,12]") }
104 |
105 | it { expect(Panko._type_cast(type, "a" => 1)).to eq("a" => 1) }
106 | it { expect(Panko._type_cast(type, [6, 12])).to eq([6, 12]) }
107 | end
108 |
109 | context "ActiveRecord::Type::Boolean" do
110 | let(:type) { ActiveRecord::Type::Boolean.new }
111 |
112 | it { expect(Panko._type_cast(type, "")).to be_nil }
113 | it { expect(Panko._type_cast(type, nil)).to be_nil }
114 |
115 | it { expect(Panko._type_cast(type, true)).to be_truthy }
116 | it { expect(Panko._type_cast(type, 1)).to be_truthy }
117 | it { expect(Panko._type_cast(type, "1")).to be_truthy }
118 | it { expect(Panko._type_cast(type, "t")).to be_truthy }
119 | it { expect(Panko._type_cast(type, "T")).to be_truthy }
120 | it { expect(Panko._type_cast(type, "true")).to be_truthy }
121 | it { expect(Panko._type_cast(type, "TRUE")).to be_truthy }
122 |
123 | it { expect(Panko._type_cast(type, false)).to be_falsey }
124 | it { expect(Panko._type_cast(type, 0)).to be_falsey }
125 | it { expect(Panko._type_cast(type, "0")).to be_falsey }
126 | it { expect(Panko._type_cast(type, "f")).to be_falsey }
127 | it { expect(Panko._type_cast(type, "F")).to be_falsey }
128 | it { expect(Panko._type_cast(type, "false")).to be_falsey }
129 | it { expect(Panko._type_cast(type, "FALSE")).to be_falsey }
130 | end
131 |
132 | context "Time" do
133 | let(:type) { ActiveRecord::Type::DateTime.new }
134 | let(:date) { DateTime.new(2017, 3, 4, 12, 45, 23) }
135 | let(:utc) { ActiveSupport::TimeZone.new("UTC") }
136 |
137 | it "ISO8601 strings" do
138 | expect(Panko._type_cast(type, date.in_time_zone(utc).as_json)).to eq("2017-03-04T12:45:23.000Z")
139 | end
140 |
141 | it "two digits after ." do
142 | expect(Panko._type_cast(type, "2018-09-16 14:51:03.97")).to eq("2018-09-16T14:51:03.970Z")
143 | end
144 |
145 | it "converts string from datbase to utc time zone" do
146 | time = "2017-07-10 09:26:40.937392"
147 | seconds = 40 + Rational(937_296, 10**6)
148 | result = DateTime.new(2017, 7, 10, 9, 26, seconds).in_time_zone(utc)
149 |
150 | expect(Panko._type_cast(type, time)).to eq(result.as_json)
151 | end
152 | end
153 | end
154 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/setup"
4 | require "panko_serializer"
5 | require "faker"
6 |
7 | require_relative "models"
8 |
9 | RSpec.configure do |config|
10 | config.order = "random"
11 |
12 | # Enable flags like --only-failures and --next-failure
13 | config.example_status_persistence_file_path = ".rspec_status"
14 |
15 | config.filter_run focus: true
16 | config.run_all_when_everything_filtered = true
17 |
18 | config.full_backtrace = ENV.fetch("CI", false)
19 |
20 | if ENV.fetch("CI", false)
21 | config.before(:example, :focus) do
22 | raise "This example was committed with `:focus` and should not have been"
23 | end
24 | end
25 |
26 | config.expect_with :rspec do |c|
27 | c.syntax = :expect
28 | end
29 |
30 | config.before do
31 | FooHolder.delete_all
32 | FoosHolder.delete_all
33 | Foo.delete_all
34 | end
35 | end
36 |
37 | RSpec::Matchers.define :serialized_as do |serializer_factory_or_class, output|
38 | serializer_factory = if serializer_factory_or_class.respond_to?(:call)
39 | serializer_factory_or_class
40 | else
41 | -> { serializer_factory_or_class.new }
42 | end
43 |
44 | match do |object|
45 | expect(serializer_factory.call.serialize(object)).to eq(output)
46 |
47 | json = Oj.load serializer_factory.call.serialize_to_json(object)
48 | expect(json).to eq(output)
49 | end
50 |
51 | failure_message do |object|
52 | <<~FAILURE
53 |
54 | Expected Output:
55 | #{output}
56 |
57 | Got:
58 |
59 | Object: #{serializer_factory.call.serialize(object)}
60 | JSON: #{Oj.load(serializer_factory.call.serialize_to_json(object))}
61 | FAILURE
62 | end
63 | end
64 |
65 | if GC.respond_to?(:verify_compaction_references)
66 | # This method was added in Ruby 3.0.0. Calling it this way asks the GC to
67 | # move objects around, helping to find object movement bugs.
68 | GC.verify_compaction_references(double_heap: true, toward: :empty)
69 | end
70 |
--------------------------------------------------------------------------------