├── .config └── tocer │ └── configuration.yml ├── .github ├── FUNDING.yml └── workflows │ ├── codeql-analysis.yml │ ├── standardrb.yml.pause │ └── tests.yml ├── .gitignore ├── .gitpod.yml ├── .standard.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── benchmark ├── benchmarks │ ├── active_record │ │ ├── encoder_vs_elt-json_deep.rb │ │ ├── encoder_vs_etl-json_shallow.rb │ │ ├── packer_vs_elt-json_deep.rb │ │ ├── packer_vs_etl-json_shallow.rb │ │ ├── packer_vs_marshal_deep.rb │ │ ├── packer_vs_marshal_shallow.rb │ │ ├── uid_vs_elt-json_deep.rb │ │ └── uid_vs_etl-json_shallow.rb │ ├── hash │ │ ├── encoder_vs_marshal_composites.rb │ │ ├── encoder_vs_marshal_scalars.rb │ │ ├── packer_vs_marshal_composites.rb │ │ ├── packer_vs_marshal_scalars.rb │ │ ├── uid_vs_marshal_composites.rb │ │ └── uid_vs_marshal_scalars.rb │ └── lib │ │ ├── runner.rb │ │ └── writer.rb ├── c ├── console ├── demo ├── loc ├── rake ├── setup ├── standardize ├── t └── test ├── config ├── default.yml └── example.yml ├── docs └── use_cases.md ├── lib ├── universalid.rb ├── universalid │ ├── encoder.rb │ ├── extensions │ │ ├── active_record │ │ │ ├── base_message_pack_type.rb │ │ │ ├── base_packer.rb │ │ │ ├── base_unpacker.rb │ │ │ └── relation_message_pack_type.rb │ │ ├── active_support │ │ │ ├── cache │ │ │ │ ├── entry_message_pack_type.rb │ │ │ │ └── store_message_pack_type.rb │ │ │ └── time_with_zone_message_pack_type.rb │ │ ├── global_id │ │ │ ├── global_id_model.rb │ │ │ ├── global_id_uid_extension.rb │ │ │ └── message_pack_type.rb │ │ └── signed_global_id │ │ │ └── message_pack_type.rb │ ├── message_pack_factory.rb │ ├── message_pack_types.rb │ ├── message_pack_types │ │ ├── composites │ │ │ ├── module.rb │ │ │ ├── open_struct.rb │ │ │ ├── set.rb │ │ │ └── struct.rb │ │ ├── scalars │ │ │ ├── bigdecimal.rb │ │ │ ├── complex.rb │ │ │ ├── date.rb │ │ │ ├── date_time.rb │ │ │ ├── range.rb │ │ │ ├── rational.rb │ │ │ └── regexp.rb │ │ └── uri │ │ │ └── uid │ │ │ └── type.rb │ ├── packer.rb │ ├── prepack_database_options.rb │ ├── prepack_options.rb │ ├── prepacker.rb │ ├── refinements.rb │ ├── refinements │ │ ├── array_refinement.rb │ │ ├── hash_refinement.rb │ │ ├── open_struct_refinement.rb │ │ └── set_refinement.rb │ ├── settings.rb │ └── version.rb └── uri │ └── uid.rb ├── test ├── rails_kit │ ├── models │ │ ├── active_record_etl.rb │ │ ├── active_record_forge.rb │ │ ├── application_record.rb │ │ ├── attachment.rb │ │ ├── campaign.rb │ │ └── email.rb │ └── setup.rb ├── test_extension.rb ├── universalid │ ├── campaign_demo_test.rb │ ├── encoder │ │ ├── active_record_test.rb │ │ ├── ruby_composites_test.rb │ │ └── ruby_scalars_test.rb │ ├── extensions │ │ ├── active_record │ │ │ ├── active_record_associations_changed_test.rb │ │ │ ├── active_record_associations_persisted_test.rb │ │ │ ├── active_record_associations_unpersisted_test.rb │ │ │ ├── active_record_changed_test.rb │ │ │ ├── active_record_persisted_test.rb │ │ │ ├── active_record_relation_test.rb │ │ │ └── active_record_unpersisted_test.rb │ │ ├── active_support │ │ │ ├── cache │ │ │ │ └── store_test.rb │ │ │ └── time_with_zone_test.rb │ │ ├── global_id │ │ │ ├── global_id_model_test.rb │ │ │ └── global_id_test.rb │ │ └── signed_global_id │ │ │ └── signed_global_id_test.rb │ ├── message_pack_types │ │ ├── composites │ │ │ ├── array_test.rb │ │ │ ├── hash_test.rb │ │ │ ├── open_struct.rb │ │ │ ├── set_test.rb │ │ │ └── struct_test.rb │ │ └── scalars │ │ │ ├── big_decimal_test.rb │ │ │ ├── complex_test.rb │ │ │ ├── date_test.rb │ │ │ ├── date_time_test.rb │ │ │ ├── false_class_test.rb │ │ │ ├── float_test.rb │ │ │ ├── integer_test.rb │ │ │ ├── nil_class_test.rb │ │ │ ├── range_test.rb │ │ │ ├── rational_test.rb │ │ │ ├── regexp_test.rb │ │ │ ├── string_test.rb │ │ │ ├── symbol_test.rb │ │ │ ├── time_test.rb │ │ │ └── true_class_test.rb │ ├── prepack_options_test.rb │ ├── prepacker │ │ ├── array_test.rb │ │ └── hash_test.rb │ ├── readme_test.rb │ ├── settings_test.rb │ └── universal_id_test.rb └── uri │ ├── uid │ └── real_world_example_test.rb │ └── uid_test.rb └── universalid.gemspec /.config/tocer/configuration.yml: -------------------------------------------------------------------------------- 1 | label: "## Table of Contents" 2 | patterns: 3 | - "README.md" 4 | root_dir: "." 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: hopsoft 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '45 17 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'ruby' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/standardrb.yml.pause: -------------------------------------------------------------------------------- 1 | name: StandardRB 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | standard: 13 | name: StandardRB Check Action 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | ruby-version: ['3.2'] 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Ruby ${{ matrix.ruby-version }} 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.ruby-version }} 24 | - uses: actions/cache@v3 25 | with: 26 | path: vendor/bundle 27 | key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-gems- 30 | - name: Bundle install 31 | run: | 32 | gem install bundler 33 | bundle config path vendor/bundle 34 | bundle install --jobs 4 --retry 3 35 | - name: Run StandardRB 36 | run: bundle exec standardrb --format progress 37 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | ruby_test: 13 | name: Ruby Test Action 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | ruby-version: ['3.0', '3.1', '3.2', '3.3'] 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Ruby ${{ matrix.ruby-version }} 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.ruby-version }} 24 | - uses: actions/cache@v3 25 | with: 26 | path: vendor/bundle 27 | key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-gems- 30 | - name: Bundle install 31 | run: | 32 | gem install bundler 33 | bundle config path vendor/bundle 34 | bundle install --jobs 4 --retry 3 35 | - name: Run ruby tests 36 | run: bundle exec rake test 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | *~ 11 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart 6 | 7 | tasks: 8 | - init: bin/setup 9 | 10 | 11 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | ruby_version: 2.7 2 | format: progress 3 | parallel: true 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Nate Hopkins (hopsoft) 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 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "minitest/test_task" 5 | require "pry-byebug" 6 | 7 | task default: :test 8 | 9 | # take explicit control of test initialization 10 | 11 | task test: [:load_tests, :exec_tests] 12 | 13 | task :load_tests do 14 | ENV["TEST_SEED"] ||= ENV.fetch("TEST_SEED", Time.now).to_s 15 | require_relative "test/test_extension" 16 | 17 | globs = ENV["GLOBS"] ? ENV["GLOBS"].split(",") : ["test/**/*_test.rb"] 18 | files = globs.map { |glob| Dir[glob] }.flatten.shuffle 19 | files.each { |file| require_relative file } 20 | end 21 | 22 | Minitest::TestTask.create(:exec_tests) do |t| 23 | t.test_globs.clear 24 | end 25 | -------------------------------------------------------------------------------- /bin/benchmark: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | srand ENV.fetch("TEST_SEED", Time.now).to_i 5 | 6 | require "objspace" 7 | require_relative "../test/rails_kit/setup" 8 | require_relative "../lib/universalid" 9 | require_relative "benchmarks/lib/runner" 10 | 11 | FILE = ARGV[0] 12 | 13 | case FILE 14 | when nil then Dir["#{__dir__}/benchmarks/**/*.rb"].shuffle.each { |file| require file } 15 | when "active_record" then Dir["#{__dir__}/benchmarks/active_record/**/*.rb"].shuffle.each { |file| require file } 16 | when "hash" then Dir["#{__dir__}/benchmarks/hash/**/*.rb"].shuffle.each { |file| require file } 17 | else require_relative "benchmarks/#{FILE}" 18 | end 19 | 20 | exit 0 21 | 22 | 23 | 24 | # seed data .................................................................................................. 25 | 26 | # benchmarks ................................................................................................. 27 | write( 28 | [ 29 | style("Benchmarking ", :cyan, :bright), 30 | style(number_with_delimiter(ITERATIONS), :lime), 31 | style(" #{"iteration".pluralize(ITERATIONS)} ", :cyan, :bright), 32 | style("with ", :cyan, :bright), 33 | style(number_with_delimiter(Campaign.count + Email.count + Attachment.count), :lime), 34 | style(" related records marshaled as an atomic singular unit", :cyan, :bright) 35 | ].join 36 | ) 37 | 38 | # ............................................................................................................ 39 | #run("UID.build Hash ....... +descendants") { URI::UID.build campaign_hash } 40 | #run("UID.build ActiveRecord +descendants") { URI::UID.build campaign, include_descendants: true, descendant_depth: 2 } 41 | #run("UID.build ActiveRecord +descendants copy") { URI::UID.build campaign, include_keys: false, include_descendants: true, descendant_depth: 2 } 42 | ## ............................................................................................................ 43 | #run("UID.build Hash ....... +descendants -blank") { URI::UID.build campaign_hash, include_blank: false } 44 | #run("UID.build ActiveRecord +descendants -blank") { URI::UID.build campaign, include_blank: false, include_descendants: true, descendant_depth: 2 } 45 | #run("UID.build ActiveRecord +descendants -blank copy") { URI::UID.build campaign, include_keys: false, include_blanks: false, include_descendants: true, descendant_depth: 2 } 46 | ## ............................................................................................................ 47 | #run("UID.parse Hash ....... +descendants") { URI::UID.parse campaign_hash_uid_string } 48 | #run("UID.parse ActiveRecord +descendants") { URI::UID.parse campaign_uid_string } 49 | ## ............................................................................................................ 50 | #run("UID.decode Hash ....... +descendants") { campaign_hash_uid.decode } 51 | #run("UID.decode ActiveRecord +descendants") { campaign_uid.decode } 52 | ## ............................................................................................................ 53 | #run("ActiveRecord → GlobalID") { campaign.to_gid_param } 54 | #run("ActiveRecord → SignedGlobalID") { campaign.to_sgid_param } 55 | #run("Encoder.encode ActiveRecord -descentants (== gid)") { UniversalID::Encoder.encode campaign } 56 | #run("UID.build ActiveRecord -descentants") { URI::UID.build campaign } 57 | #run("UID.build ActiveRecord -descentants -blank") { URI::UID.build campaign, include_blank: false } 58 | #run("UID.build ActiveRecord -descentants -blank copy") { URI::UID.build campaign, include_blank: false, include_keys: false } 59 | ## ............................................................................................................ 60 | #run("UID → GID → UID.from_gid → UID.decode +descendants") do 61 | #URI::UID.from_gid(URI::UID.build(campaign, include_descendants: true, descendant_depth: 2).to_gid_param).decode 62 | #end 63 | 64 | #run("UID → SGID → UID.from_sgid → UID.decode +descendants") do 65 | #URI::UID 66 | #.from_sgid(URI::UID.build(campaign, include_descendants: true, descendant_depth: 2) 67 | #.to_sgid_param(for: "benchmarks"), for: "benchmarks").decode 68 | #end 69 | 70 | # ............................................................................................................ 71 | 72 | #write "Marshal", :yellow, :bright, pad: "⎯", subtext: <<~DESC 73 | #Creates a deep COPY of the record and it's associations and unsaved changes using Ruby's native Marshal. 74 | #This serves as the baseline for comparison with UniversalID serialization. 75 | #NOTE: This isn't an apples/apples comparison UniversalID serialization but it serves as our baseline for now. 76 | #DESC 77 | #baseline = { payload: Marshal.dump(@campaign) } 78 | #baseline[:dump] = run("Marshal.dump") { Marshal.dump @campaign } 79 | #baseline[:load] = run("Marshal.load") { Marshal.load baseline[:payload] } 80 | #puts 81 | #puts "#{style "Payload size", :faint, width: Runner::WIDTH, pad: "."} #{number_to_human_size baseline[:payload].bytesize} #{style "(baseline)", :faint}" 82 | 83 | # ............................................................................................................ 84 | 85 | #write "UniversalID::Packer", :cyan, :bright, pad: "⎯", subtext: <<~DESC 86 | #Creates a shallow COPY of the record without associations. 87 | #Only serializes the primary key and does not include unsaved changes. 88 | #Unpack performs a database query to retrieve the record and hydrates an ActiveRecord model. 89 | #NOTE: This acts like a deep copy because the associations can be lazy loaded from the database. 90 | #DESC 91 | #result = { payload: UniversalID::Packer.pack(@campaign) } 92 | #result[:dump] = run("UniversalID::Packer.pack") { UniversalID::Packer.pack @campaign } 93 | #result[:load] = run("UniversalID::Packer.unpack") { UniversalID::Packer.unpack result[:payload] } 94 | #puts "\nComparisons" 95 | #compare "Payload size", result[:payload].bytesize, baseline[:payload].bytesize, formatted: number_to_human_size(result[:payload].bytesize.round, precision: 2), baseline_label: "Marshal.dump" 96 | #compare "UniversalID::Packer.pack", result[:dump].real, baseline[:dump].real, formatted: number_to_human(result[:dump].real, precision: 2), baseline_label: "Marshal.dump" 97 | #compare "UniversalID::Packer.unpack", result[:load].real, baseline[:load].real, formatted: number_to_human(result[:load].real, precision: 2), baseline_label: "Marshal.load" 98 | 99 | # ............................................................................................................ 100 | 101 | write "UniversalID::Packer", :cyan, :bright, pad: "⎯", subtext: <<~DESC 102 | TODO: Write this up... 103 | DESC 104 | options = { include_descendants: true, descendant_depth: 2 } 105 | result = { payload: UniversalID::Packer.pack(@campaign, options) } 106 | result[:dump] = run ("UniversalID::Packer.pack") { UniversalID::Packer.pack @campaign, options } 107 | result[:load] = run ("UniversalID::Packer.pack") { UniversalID::Packer.unpack result[:payload] } 108 | puts "\nComparisons" 109 | compare "Payload size", result[:payload].bytesize, baseline[:payload].bytesize, formatted: number_to_human_size(result[:payload].bytesize.round, precision: 2), baseline_label: "Marshal.dump" 110 | compare "UniversalID::Packer.pack", result[:dump].real, baseline[:dump].real, formatted: number_to_human(result[:dump].real, precision: 2), baseline_label: "Marshal.dump" 111 | compare "UniversalID::Packer.unpack", result[:load].real, baseline[:load].real, formatted: number_to_human(result[:load].real, precision: 2), baseline_label: "Marshal.load" 112 | 113 | # ............................................................................................................ 114 | 115 | #publish Rainbow("ActiveRecord -pks, -fks, -timestamps ".ljust(98, ".")).lime.bright do 116 | #options = {include_keys: false, include_timestamps: false} 117 | #packed = UniversalID::Packer.pack(@campaign, options) 118 | #run("UniversalID::Packer.pack") { UniversalID::Packer.pack @campaign, options } 119 | #run("UniversalID::Packer.unpack") { UniversalID::Packer.unpack packed } 120 | #end 121 | 122 | #publish Rainbow("ActiveRecord -pks, -fks, -timestamps, +descendants ".ljust(98, ".")).lime.bright do 123 | #options = {include_keys: false, include_timestamps: false, include_descendants: true, descendant_depth: 2} 124 | #packed = UniversalID::Packer.pack(@campaign, options) 125 | #run("UniversalID::Packer.pack") { UniversalID::Packer.pack @campaign, options } 126 | #run("UniversalID::Packer.unpack") { UniversalID::Packer.unpack packed } 127 | #end 128 | -------------------------------------------------------------------------------- /bin/benchmarks/active_record/encoder_vs_elt-json_deep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | runner = Runner.new desc: <<-DESC 4 | Encodes an ActiveRecord with it's loaded associations, then decodes the payload. 5 | 6 | Benchmark: 7 | - dump: UniversalID::Encoder.encode subject, include_descendants: true, descendant_depth: 2 8 | - load: UniversalID::Encoder.decode payload 9 | 10 | Control: 11 | - dump: ActiveRecordETL::Pipeline.new(subject).transform nested_attributes: true 12 | - load: (apples -vs- oranges) 13 | UID implicitly does a lot under the hood 14 | I approximate that behavior for control load 15 | See the benchmark file for details 16 | DESC 17 | 18 | # serialize (control) ........................................................................................ 19 | runner.control_dump "ActiveRecordETL::Pipeline#tranform" do 20 | subject.transform nested_attributes: true 21 | end 22 | 23 | # deserialize (control) ...................................................................................... 24 | runner.control_load "ActiveRecordETL.parse + AR find(id)" do 25 | parsed = ActiveRecordETL.parse(payload) 26 | campaign = subject.class.find_by(id: parsed["id"]) 27 | campaign.assign_attributes parsed.except("id", "emails_attributes") 28 | campaign.emails.load 29 | parsed["emails_attributes"].each do |email_attributes| 30 | email = campaign.emails.find { |e| e.id == email_attributes["id"] } 31 | email.assign_attributes email_attributes.except("id", "attachments_attributes") 32 | email.attachments.load 33 | email_attributes["attachments_attributes"].each do |attachment_attributes| 34 | attachment = email.attachments.find { |a| a.id == attachment_attributes["id"] } 35 | attachment.assign_attributes attachment_attributes.except("id") 36 | end 37 | end 38 | campaign 39 | end 40 | 41 | # serialize .................................................................................................. 42 | runner.run_dump("UniversalID::Packer.pack") do 43 | UniversalID::Encoder.encode subject, include_descendants: true, descendant_depth: 2 44 | end 45 | 46 | # deserialize ................................................................................................ 47 | runner.run_load("UniversalID::Packer.unpack") do 48 | UniversalID::Encoder.decode payload 49 | end 50 | -------------------------------------------------------------------------------- /bin/benchmarks/active_record/encoder_vs_etl-json_shallow.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | runner = Runner.new desc: <<-DESC 4 | Encodes an ActiveRecord (id only), then decodes the payload. 5 | 6 | Benchmark: 7 | - serialize: UniversalID::Encoder.encode subject 8 | - deserialize: UniversalID::Encoder.decode payload 9 | 10 | Control: 11 | - dump: ActiveRecordETL::Pipeline.new(subject).transform only: ["id"] 12 | - load: subject.class.find_by id: ActiveRecordETL.parse(payload)["id"] 13 | DESC 14 | 15 | # serialize (control) ........................................................................................ 16 | runner.control_dump "ActiveRecordETL::Pipeline#tranform (id only)" do 17 | subject.transform only: ["id"] 18 | end 19 | 20 | # deserialize (control) ...................................................................................... 21 | runner.control_load "ActiveRecordETL.parse + AR find(id)" do 22 | subject.class.find_by id: ActiveRecordETL.parse(payload)["id"] 23 | end 24 | 25 | # serialize .................................................................................................. 26 | runner.run_dump("UniversalID::Packer.pack") do 27 | UniversalID::Encoder.encode subject 28 | end 29 | 30 | # deserialize ................................................................................................ 31 | runner.run_load("UniversalID::Packer.unpack") do 32 | UniversalID::Encoder.decode payload 33 | end 34 | -------------------------------------------------------------------------------- /bin/benchmarks/active_record/packer_vs_elt-json_deep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | runner = Runner.new desc: <<-DESC 4 | Packs an ActiveRecord with it's loaded associations, then unpacks the payload. 5 | 6 | Benchmark: 7 | - dump: UniversalID::Packer.pack subject, 8 | include_descendants: true, descendant_depth: 2 9 | - load: UniversalID::Packer.unpack payload 10 | 11 | Control: 12 | - dump: ActiveRecordETL::Pipeline.new(subject).transform nested_attributes: true 13 | - load: (apples -vs- oranges) 14 | UID implicitly does a lot under the hood 15 | I approximate that behavior for control load 16 | See the benchmark file for details 17 | DESC 18 | 19 | # serialize (control) ........................................................................................ 20 | runner.control_dump "ActiveRecordETL::Pipeline#tranform" do 21 | subject.transform nested_attributes: true 22 | end 23 | 24 | # deserialize (control) ...................................................................................... 25 | runner.control_load "ActiveRecordETL.parse + AR find(id)" do 26 | parsed = ActiveRecordETL.parse(payload) 27 | campaign = subject.class.find_by(id: parsed["id"]) 28 | campaign.assign_attributes parsed.except("id", "emails_attributes") 29 | campaign.emails.load 30 | parsed["emails_attributes"].each do |email_attributes| 31 | email = campaign.emails.find { |e| e.id == email_attributes["id"] } 32 | email.assign_attributes email_attributes.except("id", "attachments_attributes") 33 | email.attachments.load 34 | email_attributes["attachments_attributes"].each do |attachment_attributes| 35 | attachment = email.attachments.find { |a| a.id == attachment_attributes["id"] } 36 | attachment.assign_attributes attachment_attributes.except("id") 37 | end 38 | end 39 | campaign 40 | end 41 | 42 | # serialize .................................................................................................. 43 | runner.run_dump("UniversalID::Packer.pack") do 44 | UniversalID::Packer.pack subject, include_descendants: true, descendant_depth: 2 45 | end 46 | 47 | # deserialize ................................................................................................ 48 | runner.run_load("UniversalID::Packer.unpack") do 49 | UniversalID::Packer.unpack payload 50 | end 51 | -------------------------------------------------------------------------------- /bin/benchmarks/active_record/packer_vs_etl-json_shallow.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | runner = Runner.new desc: <<-DESC 4 | Packs an ActiveRecord (id only), then unpacks the payload. 5 | 6 | Benchmark: 7 | - dump: UniversalID::Packer.pack subject 8 | - load: UniversalID::Packer.unpack payload 9 | 10 | Control: 11 | - dump: ActiveRecordETL::Pipeline.new(subject).transform only: ["id"] 12 | - load: subject.class.find_by id: ActiveRecordETL.parse(payload)["id"] 13 | DESC 14 | 15 | # serialize (control) ........................................................................................ 16 | runner.control_dump "ActiveRecordETL::Pipeline#tranform (id only)" do 17 | subject.transform only: ["id"] 18 | end 19 | 20 | # deserialize (control) ...................................................................................... 21 | runner.control_load "ActiveRecordETL.parse + AR find(id)" do 22 | subject.class.find_by id: ActiveRecordETL.parse(payload)["id"] 23 | end 24 | 25 | # serialize .................................................................................................. 26 | runner.run_dump("UniversalID::Packer.pack") do 27 | UniversalID::Packer.pack subject 28 | end 29 | 30 | # deserialize ................................................................................................ 31 | runner.run_load("UniversalID::Packer.unpack") do 32 | UniversalID::Packer.unpack payload 33 | end 34 | -------------------------------------------------------------------------------- /bin/benchmarks/active_record/packer_vs_marshal_deep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | runner = Runner.new desc: <<-DESC 4 | Packs an ActiveRecord with it's loaded associations, then unpacks the payload. 5 | 6 | Benchmark: 7 | - dump: UniversalID::Packer.pack subject, 8 | include_descendants: true, descendant_depth: 2 9 | - load: UniversalID::Packer.unpack payload 10 | 11 | Control: 12 | - dump: Marshal.dump subject 13 | - load: Marshal.load payload 14 | DESC 15 | 16 | # serialize (control) ........................................................................................ 17 | runner.control_dump "Marshal.dump" do 18 | Marshal.dump subject 19 | end 20 | 21 | # deserialize (control) ...................................................................................... 22 | runner.control_load "Marshal.load" do 23 | Marshal.load control_payload 24 | end 25 | 26 | # serialize .................................................................................................. 27 | runner.run_dump("UniversalID::Packer.pack") do 28 | UniversalID::Packer.pack subject, include_descendants: true, descendant_depth: 2 29 | end 30 | 31 | # deserialize ................................................................................................ 32 | runner.run_load("UniversalID::Packer.unpack") do 33 | UniversalID::Packer.unpack payload 34 | end 35 | -------------------------------------------------------------------------------- /bin/benchmarks/active_record/packer_vs_marshal_shallow.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | runner = Runner.new desc: <<-DESC 4 | Packs an ActiveRecord (id only), then unpacks the payload. 5 | 6 | Benchmark: 7 | - dump: UniversalID::Packer.pack subject 8 | - load: UniversalID::Packer.unpack payload 9 | 10 | Control: 11 | - dump: Marshal.dump subject.attributes.slice("id") 12 | - load: subject.class.find_by id: Marshal.load(payload)["id"] 13 | DESC 14 | 15 | # serialize (control) ........................................................................................ 16 | runner.control_dump "Marshal.dump (id only)" do 17 | Marshal.dump subject.attributes.slice("id") 18 | end 19 | 20 | # deserialize (control) ...................................................................................... 21 | runner.control_load "Marshal.load + AR find(id)" do 22 | subject.class.find_by id: Marshal.load(control_payload)["id"] 23 | end 24 | 25 | # serialize .................................................................................................. 26 | runner.run_dump("UniversalID::Packer.pack") do 27 | UniversalID::Packer.pack subject 28 | end 29 | 30 | # deserialize ................................................................................................ 31 | runner.run_load("UniversalID::Packer.unpack") do 32 | UniversalID::Packer.unpack payload 33 | end 34 | -------------------------------------------------------------------------------- /bin/benchmarks/active_record/uid_vs_elt-json_deep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | runner = Runner.new desc: <<-DESC 4 | Builds a UID for an ActiveRecord with it's loaded associations, 5 | then parses the UID and decodes the payload. 6 | 7 | Benchmark: 8 | - serialize: URI::UID.build(subject, include_descendants: true, descendant_depth: 2).to_s 9 | - deserialize: URI::UID.parse(payload).decode 10 | 11 | Control: 12 | - dump: ActiveRecordETL::Pipeline.new(subject).transform nested_attributes: true 13 | - load: (apples -vs- oranges) 14 | UID implicitly does a lot under the hood 15 | I approximate that behavior for control load 16 | See the benchmark file for details 17 | DESC 18 | 19 | # serialize (control) ........................................................................................ 20 | runner.control_dump "ActiveRecordETL::Pipeline#tranform" do 21 | subject.transform nested_attributes: true 22 | end 23 | 24 | # deserialize (control) ...................................................................................... 25 | runner.control_load "ActiveRecordETL.parse + AR find(id)" do 26 | parsed = ActiveRecordETL.parse(payload) 27 | campaign = subject.class.find_by(id: parsed["id"]) 28 | campaign.assign_attributes parsed.except("id", "emails_attributes") 29 | campaign.emails.load 30 | parsed["emails_attributes"].each do |email_attributes| 31 | email = campaign.emails.find { |e| e.id == email_attributes["id"] } 32 | email.assign_attributes email_attributes.except("id", "attachments_attributes") 33 | email.attachments.load 34 | email_attributes["attachments_attributes"].each do |attachment_attributes| 35 | attachment = email.attachments.find { |a| a.id == attachment_attributes["id"] } 36 | attachment.assign_attributes attachment_attributes.except("id") 37 | end 38 | end 39 | campaign 40 | end 41 | 42 | # serialize .................................................................................................. 43 | runner.run_dump("UniversalID::Packer.pack") do 44 | URI::UID.build(subject, include_descendants: true, descendant_depth: 2).to_s 45 | end 46 | 47 | # deserialize ................................................................................................ 48 | runner.run_load("UniversalID::Packer.unpack") do 49 | URI::UID.parse(payload).decode 50 | end 51 | -------------------------------------------------------------------------------- /bin/benchmarks/active_record/uid_vs_etl-json_shallow.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | runner = Runner.new desc: <<-DESC 4 | Builds a UID for an ActiveRecord (id only), then parses the UID and decodes the payload. 5 | 6 | Benchmark: 7 | - serialize: URI::UID.build(subject).to_s 8 | - deserialize: URI::UID.parse(payload).decode 9 | 10 | Control: 11 | - dump: ActiveRecordETL::Pipeline.new(subject).transform only: ["id"] 12 | - load: subject.class.find_by id: ActiveRecordETL.parse(payload)["id"] 13 | DESC 14 | 15 | # serialize (control) ........................................................................................ 16 | runner.control_dump "ActiveRecordETL::Pipeline#tranform (id only)" do 17 | subject.transform only: ["id"] 18 | end 19 | 20 | # deserialize (control) ...................................................................................... 21 | runner.control_load "ActiveRecordETL.parse + AR find(id)" do 22 | subject.class.find_by id: ActiveRecordETL.parse(payload)["id"] 23 | end 24 | 25 | # serialize .................................................................................................. 26 | runner.run_dump("UniversalID::Packer.pack") do 27 | URI::UID.build(subject).to_s 28 | end 29 | 30 | # deserialize ................................................................................................ 31 | runner.run_load("UniversalID::Packer.unpack") do 32 | URI::UID.parse(payload).decode 33 | end 34 | -------------------------------------------------------------------------------- /bin/benchmarks/hash/encoder_vs_marshal_composites.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | scalars = { 4 | big_decimal: BigDecimal("123.45"), 5 | complex: Complex(1, 2), 6 | date: Date.today, 7 | datetime: DateTime.now, 8 | false_class: false, 9 | float: 123.45, 10 | integer: 123, 11 | nil_class: nil, 12 | # range: 1..100, # TODO: Marshal.load fails on the Range 13 | rational: Rational(3, 4), 14 | regexp: /abc/, 15 | string: "hello", 16 | symbol: :symbol, 17 | time: Time.now, 18 | true_class: true 19 | } 20 | 21 | NamedStructEncoder = Struct.new(*scalars.keys) 22 | 23 | composites = { 24 | array: scalars.values, 25 | hash: scalars, 26 | open_struct: OpenStruct.new(scalars), 27 | set: Set.new(scalars.values), 28 | struct: NamedStructEncoder.new(*scalars.values) 29 | } 30 | 31 | 3.times do |i| 32 | composites[:nested_array] ||= [] 33 | composites[:nested_array] << composites.deep_dup 34 | composites[:"nested_hash_#{i}"] = composites.deep_dup 35 | end 36 | 37 | # ............................................................................................................ 38 | 39 | runner = Runner.new subject: composites, desc: <<-DESC 40 | Encodes a deeply nested Ruby Hash that contains Composite (i.e. compound) values, 41 | then decodes the payload. 42 | 43 | Benchmark: 44 | - serialize: UniversalID::Encoder.encode subject 45 | - deserialize: UniversalID::Encoder.decode payload 46 | 47 | Control: 48 | - serialize: Marshal.dump subject 49 | - deserialize: Marshal.load payload 50 | DESC 51 | 52 | # serialize (control) ........................................................................................ 53 | runner.control_dump "Marshal.dump" do 54 | Marshal.dump subject 55 | end 56 | 57 | # deserialize (control) ...................................................................................... 58 | runner.control_load "Marshal.load" do 59 | Marshal.load control_payload 60 | end 61 | 62 | # serialize .................................................................................................. 63 | runner.run_dump("UniversalID::Encoder.encode") do 64 | UniversalID::Encoder.encode subject 65 | end 66 | 67 | # deserialize ................................................................................................ 68 | runner.run_load("UniversalID::Encoder.decode") do 69 | UniversalID::Encoder.decode payload 70 | end 71 | -------------------------------------------------------------------------------- /bin/benchmarks/hash/encoder_vs_marshal_scalars.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | scalars = { 4 | big_decimal: BigDecimal("123.45"), 5 | complex: Complex(1, 2), 6 | date: Date.today, 7 | date_time: DateTime.now, 8 | false_class: false, 9 | float: 123.45, 10 | integer: 123, 11 | nil_class: nil, 12 | range: 1..100, 13 | rational: Rational(3, 4), 14 | regexp: /abc/, 15 | string: "hello", 16 | symbol: :symbol, 17 | time: Time.now, 18 | true_class: true 19 | } 20 | 21 | runner = Runner.new subject: scalars, desc: <<-DESC 22 | Encodes a Ruby Hash that contains Scalar (i.e. primitive) values, 23 | then decodes the payload. 24 | 25 | Benchmark: 26 | - serialize: UniversalID::Encoder.encode subject 27 | - deserialize: UniversalID::Encoder.decode payload 28 | 29 | Control: 30 | - serialize: Marshal.dump subject 31 | - deserialize: Marshal.load payload 32 | DESC 33 | 34 | # serialize (control) ........................................................................................ 35 | runner.control_dump "Marshal.dump" do 36 | Marshal.dump subject 37 | end 38 | 39 | # deserialize (control) ...................................................................................... 40 | runner.control_load "Marshal.load" do 41 | Marshal.load control_payload 42 | end 43 | 44 | # serialize .................................................................................................. 45 | runner.run_dump("UniversalID::Encoder.encode") do 46 | UniversalID::Encoder.encode subject 47 | end 48 | 49 | # deserialize ................................................................................................ 50 | runner.run_load("UniversalID::Encoder.decode") do 51 | UniversalID::Encoder.decode payload 52 | end 53 | -------------------------------------------------------------------------------- /bin/benchmarks/hash/packer_vs_marshal_composites.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | scalars = { 4 | big_decimal: BigDecimal("123.45"), 5 | complex: Complex(1, 2), 6 | date: Date.today, 7 | datetime: DateTime.now, 8 | false_class: false, 9 | float: 123.45, 10 | integer: 123, 11 | nil_class: nil, 12 | # range: 1..100, # TODO: Marshal.load fails on the Range 13 | rational: Rational(3, 4), 14 | regexp: /abc/, 15 | string: "hello", 16 | symbol: :symbol, 17 | time: Time.now, 18 | true_class: true 19 | } 20 | 21 | NamedStructPacker = Struct.new(*scalars.keys) 22 | 23 | composites = { 24 | array: scalars.values, 25 | hash: scalars, 26 | open_struct: OpenStruct.new(scalars), 27 | set: Set.new(scalars.values), 28 | struct: NamedStructPacker.new(*scalars.values) 29 | } 30 | 31 | 3.times do |i| 32 | composites[:nested_array] ||= [] 33 | composites[:nested_array] << composites.deep_dup 34 | composites[:"nested_hash_#{i}"] = composites.deep_dup 35 | end 36 | 37 | # ............................................................................................................ 38 | 39 | runner = Runner.new subject: composites, desc: <<-DESC 40 | Packs a deeply nested Ruby Hash that contains Composite (i.e. compound) values, 41 | then unpacks the payload. 42 | 43 | Benchmark: 44 | - serialize: UniversalID::Packer.pack subject 45 | - deserialize: UniversalID::Packer.unpack payload 46 | 47 | Control: 48 | - serialize: Marshal.dump subject 49 | - deserialize: Marshal.load payload 50 | DESC 51 | 52 | # serialize (control) ........................................................................................ 53 | runner.control_dump "Marshal.dump" do 54 | Marshal.dump subject 55 | end 56 | 57 | # deserialize (control) ...................................................................................... 58 | runner.control_load "Marshal.load" do 59 | Marshal.load control_payload 60 | end 61 | 62 | # serialize .................................................................................................. 63 | runner.run_dump("UniversalID::Packer.pack") do 64 | UniversalID::Packer.pack subject 65 | end 66 | 67 | # deserialize ................................................................................................ 68 | runner.run_load("UniversalID::Packer.unpack") do 69 | UniversalID::Packer.unpack payload 70 | end 71 | -------------------------------------------------------------------------------- /bin/benchmarks/hash/packer_vs_marshal_scalars.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | scalars = { 4 | big_decimal: BigDecimal("123.45"), 5 | complex: Complex(1, 2), 6 | date: Date.today, 7 | date_time: DateTime.now, 8 | false_class: false, 9 | float: 123.45, 10 | integer: 123, 11 | nil_class: nil, 12 | range: 1..100, 13 | rational: Rational(3, 4), 14 | regexp: /abc/, 15 | string: "hello", 16 | symbol: :symbol, 17 | time: Time.now, 18 | true_class: true 19 | } 20 | 21 | runner = Runner.new subject: scalars, desc: <<-DESC 22 | Packs a Ruby Hash that contains Scalar (i.e. primitive) values, 23 | then unpacks the payload. 24 | 25 | Benchmark: 26 | - serialize: UniversalID::Packer.pack subject 27 | - deserialize: UniversalID::Packer.unpack payload 28 | 29 | Control: 30 | - serialize: Marshal.dump subject 31 | - deserialize: Marshal.load payload 32 | DESC 33 | 34 | # serialize (control) ........................................................................................ 35 | runner.control_dump "Marshal.dump" do 36 | Marshal.dump subject 37 | end 38 | 39 | # deserialize (control) ...................................................................................... 40 | runner.control_load "Marshal.load" do 41 | Marshal.load control_payload 42 | end 43 | 44 | # serialize .................................................................................................. 45 | runner.run_dump("UniversalID::Packer.pack") do 46 | UniversalID::Packer.pack subject 47 | end 48 | 49 | # deserialize ................................................................................................ 50 | runner.run_load("UniversalID::Packer.unpack") do 51 | UniversalID::Packer.unpack payload 52 | end 53 | -------------------------------------------------------------------------------- /bin/benchmarks/hash/uid_vs_marshal_composites.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | scalars = { 4 | big_decimal: BigDecimal("123.45"), 5 | complex: Complex(1, 2), 6 | date: Date.today, 7 | datetime: DateTime.now, 8 | false_class: false, 9 | float: 123.45, 10 | integer: 123, 11 | nil_class: nil, 12 | # range: 1..100, # TODO: Marshal.load fails on the Range 13 | rational: Rational(3, 4), 14 | regexp: /abc/, 15 | string: "hello", 16 | symbol: :symbol, 17 | time: Time.now, 18 | true_class: true 19 | } 20 | 21 | NamedStructUID = Struct.new(*scalars.keys) 22 | 23 | composites = { 24 | array: scalars.values, 25 | hash: scalars, 26 | open_struct: OpenStruct.new(scalars), 27 | set: Set.new(scalars.values), 28 | struct: NamedStructUID.new(*scalars.values) 29 | } 30 | 31 | 3.times do |i| 32 | composites[:nested_array] ||= [] 33 | composites[:nested_array] << composites.deep_dup 34 | composites[:"nested_hash_#{i}"] = composites.deep_dup 35 | end 36 | 37 | # ............................................................................................................ 38 | 39 | runner = Runner.new subject: composites, desc: <<-DESC 40 | Builds a UID for a Ruby Hash that contains Composite (i.e. compound) values, 41 | then parses the UID and decodes the payload. 42 | 43 | Benchmark: 44 | - serialize: URI::UID.build(subject).to_s 45 | - deserialize: URI::UID.parse(payload).decode 46 | 47 | Control: 48 | - serialize: Marshal.dump subject 49 | - deserialize: Marshal.load payload 50 | DESC 51 | 52 | # serialize (control) ........................................................................................ 53 | runner.control_dump "Marshal.dump" do 54 | Marshal.dump subject 55 | end 56 | 57 | # deserialize (control) ...................................................................................... 58 | runner.control_load "Marshal.load" do 59 | Marshal.load control_payload 60 | end 61 | 62 | # serialize .................................................................................................. 63 | runner.run_dump("UniversalID::Encoder.encode") do 64 | UniversalID::Encoder.encode subject 65 | end 66 | 67 | # deserialize ................................................................................................ 68 | runner.run_load("UniversalID::Encoder.decode") do 69 | UniversalID::Encoder.decode payload 70 | end 71 | -------------------------------------------------------------------------------- /bin/benchmarks/hash/uid_vs_marshal_scalars.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | scalars = { 4 | big_decimal: BigDecimal("123.45"), 5 | complex: Complex(1, 2), 6 | date: Date.today, 7 | date_time: DateTime.now, 8 | false_class: false, 9 | float: 123.45, 10 | integer: 123, 11 | nil_class: nil, 12 | range: 1..100, 13 | rational: Rational(3, 4), 14 | regexp: /abc/, 15 | string: "hello", 16 | symbol: :symbol, 17 | time: Time.now, 18 | true_class: true 19 | } 20 | 21 | runner = Runner.new subject: scalars, desc: <<-DESC 22 | Builds a UID for a Ruby Hash that contains Scalar (i.e. primitive) values, 23 | then parses the UID and decodes the payload. 24 | 25 | Benchmark: 26 | - serialize: URI::UID.build(subject).to_s 27 | - deserialize: URI::UID.parse(payload).decode 28 | 29 | Control: 30 | - serialize: Marshal.dump subject 31 | - deserialize: Marshal.load payload 32 | DESC 33 | 34 | # serialize (control) ........................................................................................ 35 | runner.control_dump "Marshal.dump" do 36 | Marshal.dump subject 37 | end 38 | 39 | # deserialize (control) ...................................................................................... 40 | runner.control_load "Marshal.load" do 41 | Marshal.load control_payload 42 | end 43 | 44 | # serialize .................................................................................................. 45 | runner.run_dump("URI::UID.build + to_s") do 46 | URI::UID.build(subject).to_s 47 | end 48 | 49 | # deserialize ................................................................................................ 50 | runner.run_load("URI::UID.parse + decode") do 51 | URI::UID.parse(payload).decode 52 | end 53 | -------------------------------------------------------------------------------- /bin/benchmarks/lib/writer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "action_view/helpers/number_helper" 4 | 5 | module Writer 6 | extend self 7 | include ActionView::Helpers::NumberHelper 8 | 9 | LINE_WIDTH = 110 10 | LABEL_WIDTH = 65 11 | MOVE_UP = "\e[A" 12 | REPLACE = "\e[K" 13 | 14 | def puts(value = nil, *styles, line: nil, replace: false) 15 | $stdout.puts "#{MOVE_UP}#{REPLACE}" if replace 16 | $stdout.puts style(value, *styles, line: line) 17 | end 18 | 19 | def print(value = nil, *styles, line: nil, replace: false) 20 | $stdout.puts "#{MOVE_UP}#{REPLACE}" if replace 21 | $stdout.print style(value, *styles, line: line) 22 | end 23 | 24 | def style(value, *styles, line: nil, width: LINE_WIDTH) 25 | styled = Rainbow(value.to_s) 26 | styles.each { |modifier| styled = styled.public_send(modifier) } 27 | styled += line(*styles, char: line, head: " ", width: width - value.to_s.length) if line 28 | styled 29 | end 30 | 31 | def line(*styles, char: "–", head: "", tail: "", width: LINE_WIDTH) 32 | style "#{head}#{"".ljust width.floor - head.length - tail.length, char}#{tail}", *(styles - [:bright] + [:faint]) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /bin/c: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | exec File.expand_path("./console", __dir__), *ARGV 4 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "active_record" 5 | require "amazing_print" 6 | require "pry-byebug" 7 | require "pry-doc" 8 | require "rails" 9 | require_relative "../test/rails_kit/setup" 10 | 11 | AmazingPrint.pry! 12 | Pry.start 13 | -------------------------------------------------------------------------------- /bin/demo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "amazing_print" 5 | require "pry-byebug" 6 | require "pry-doc" 7 | require "universalid" 8 | require_relative "../test/models" 9 | 10 | ## ............................................................................................................ 11 | ## Create a Campaign via multi-step form (wizard) running over HTTP 12 | ## ............................................................................................................ 13 | 14 | #binding.pry 15 | 16 | ## Step 1. Assign basic campaign info 17 | #campaign = Campaign.new(name: "Example Campaign", description: "Example Description") 18 | #param = campaign.to_packable.to_gid_param 19 | 20 | #binding.pry 21 | 22 | ## Step 2. Create first email 23 | #campaign = Campaign.from_packable(param) 24 | #campaign.emails << campaign.emails.build(subject: "First Email", body: "Welcome", wait: 1.day) 25 | #param = campaign.to_packable(methods: :emails_attributes).to_gid_param 26 | 27 | #binding.pry 28 | 29 | ## Step 3. Create second email 30 | #campaign = Campaign.from_packable(param) 31 | #campaign.emails << campaign.emails.build(subject: "Second Email", body: "Follow Up", wait: 1.week) 32 | #param = campaign.to_packable(methods: :emails_attributes).to_gid_param 33 | 34 | #binding.pry 35 | 36 | ## Step 4. Create third email 37 | #campaign = Campaign.from_packable(param) 38 | #campaign.emails << campaign.emails.build(subject: "Third Email", body: "Hard Sell", wait: 2.days) 39 | #param = campaign.to_packable(methods: :emails_attributes).to_gid_param 40 | 41 | #binding.pry 42 | 43 | ## Step 5. Configure final details 44 | #campaign = Campaign.from_packable(param) 45 | #campaign.assign_attributes trigger: "Sign Up" 46 | #param = campaign.to_packable(methods: :emails_attributes).to_gid_param 47 | 48 | #binding.pry 49 | 50 | ## Step 6. Review and save 51 | #campaign = Campaign.from_packable(param) 52 | #campaign.save! 53 | 54 | #binding.pry 55 | 56 | ## ............................................................................................................ 57 | ## Create a digital product from the Campaign (i.e. template) 58 | ## ............................................................................................................ 59 | 60 | #binding.pry 61 | 62 | ## 1. Create a packable digital product from the Campaign ..................................................... 63 | #campaign = Campaign.first 64 | #signed_param = campaign.to_packable(except: [:id, :campaign_id], methods: :emails_attributes).to_sgid_param(for: "Promotion 123", expires_in: 30.seconds) 65 | ## NOTE: The signed param is a sellable digital product with built in purpose and scarcity! 66 | 67 | ## 2. Reconstruct the shared template (digital product) ....................................................... 68 | #binding.pry 69 | #copy = Campaign.from_packable(signed_param, for: "Promotion 123") 70 | 71 | #binding.pry 72 | 73 | ## 3. Let the product expire (wait 30 seconds) ................................................................ 74 | ## gid = UniversalID::PackableHash.parse_gid(signed_param, for: "Promotion 123") 75 | #invalid_copy = Campaign.from_packable(signed_param, for: "Promotion 123") 76 | 77 | #binding.pry 78 | 79 | #puts "End of demo." 80 | -------------------------------------------------------------------------------- /bin/loc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cloc lib 4 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bundle exec rake "$@" 4 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /bin/standardize: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bundle exec magic_frozen_string_literal 4 | bundle exec standardrb --fix 5 | -------------------------------------------------------------------------------- /bin/t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | exec File.expand_path("./test", __dir__), *ARGV 4 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | globs = if ARGV.size > 1 4 | ARGV 5 | elsif ARGV.size == 1 6 | fuzzy = ARGV.first 7 | fuzzy.end_with?("_test.rb") ? 8 | ["test/**/*#{fuzzy}"] : 9 | ["test/**/*#{fuzzy}**/*_test.rb", "test/**/*#{fuzzy}*_test.rb"] 10 | end 11 | 12 | if globs.nil? 13 | exec "bundle exec rake test" 14 | else 15 | exec "bundle exec rake test GLOBS='#{globs.join(",")}'" 16 | end 17 | -------------------------------------------------------------------------------- /config/default.yml: -------------------------------------------------------------------------------- 1 | --- 2 | prepack: 3 | exclude: [] 4 | include: [] 5 | include_blank: true 6 | 7 | database: 8 | include_keys: true 9 | include_timestamps: true 10 | include_changes: false 11 | include_descendants: false 12 | descendant_depth: 0 13 | -------------------------------------------------------------------------------- /config/example.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ############################################################################################################## 3 | # Prepack options applied before packing with MessagePack 4 | ############################################################################################################## 5 | prepack: 6 | # .......................................................................................................... 7 | # A list of attributes to exclude (for objects like Hash, OpenStruct, Struct, etc.) 8 | # Takes prescedence over the`include` list 9 | exclude: [] 10 | 11 | # .......................................................................................................... 12 | # A list of attributes to include (for objects like Hash, OpenStruct, Struct, etc.) 13 | include: [] 14 | 15 | # .......................................................................................................... 16 | # Whether or not to include blank values when packing (nil, {}, [], "", etc.) 17 | include_blank: true 18 | 19 | # ========================================================================================================== 20 | # Database records 21 | database: 22 | # ...................................................................................................... 23 | # Whether or not to include primary/foreign keys 24 | # Setting this to `false` can be used to make a copy of an existing record 25 | include_keys: true 26 | 27 | # ...................................................................................................... 28 | # Whether or not to include date/time timestamps (created_at, updated_at, etc.) 29 | # Setting this to `false` can be used to make a copy of an existing record 30 | include_timestamps: true 31 | 32 | # ...................................................................................................... 33 | # Whether or not to include unsaved changes 34 | # Assign to `true` when packing new records 35 | include_changes: false 36 | 37 | # ...................................................................................................... 38 | # Whether or not to include loaded in-memory descendants (i.e. child associations) 39 | include_descendants: false 40 | 41 | # ...................................................................................................... 42 | # The max depth (number) of loaded in-memory descendants to include when `include_descendants == true` 43 | # For example, a value of (2) would include the following: 44 | # Parent > Child > Grandchild 45 | descendant_depth: 0 46 | -------------------------------------------------------------------------------- /docs/use_cases.md: -------------------------------------------------------------------------------- 1 | # Universal ID 2 | 3 | Universal ID unlocks a lot of **NEW** possibile solutions across a variety of problem domains. 4 | 5 | > [!NOTE] 6 | > _The following use-cases demonstrate the power and versatility of Universal ID._ 7 | > _You'll discover that Universal ID supports **NEW** and creative solutions that will help improve efficiency, reliability, and the end-user experience._ 8 | 9 | ## Use Cases 10 | 11 | **Consider some of these use-cases to jump start your imagination and creativity.** 12 | 13 | - **State Management for Web Applications**: 14 | Facilitate seamless user experiences in web applications by preserving and transferring UI states, even across different sessions. 15 | 16 | - **Data Serialization for Distributed Systems**: 17 | Enable efficient communication in distributed systems by serializing complex data structures for network transmission. 18 | 19 | - **Configuration Settings for Software Applications**: 20 | Store and manage configuration settings for software applications, allowing easy transfer and versioning of settings across installations. 21 | 22 | - **Session Continuity in Cloud Services**: 23 | Ensure continuity of user sessions in cloud-based applications, enabling users to pick up their work exactly where they left off, regardless of the device or location. 24 | 25 | - **Audit Logging for Complex Transactions**: 26 | Record detailed states of complex transactions in audit logs, providing a comprehensive and reversible record of actions for compliance and analysis. 27 | 28 | - **Machine-to-Machine Communication**: 29 | Standardize data formats for machine-to-machine communication, facilitating interoperability and data exchange in IoT and other automated systems. 30 | 31 | - **API Response Caching** 32 | Cache complex API responses as serialized strings, allowing for efficient storage and quick retrieval. 33 | 34 | - **Asset Management in Enterprises** 35 | Serialize asset information, including status and location, for efficient tracking and management. 36 | 37 | - **Audit Logging for Financial Transactions** 38 | Serialize transaction states for audit trails in financial applications, providing detailed and reversible records for compliance. 39 | 40 | - **Automated Testing of Web Applications** 41 | Serialize application states to reproduce and test various scenarios automatically. 42 | 43 | - **Backup and Restore of Application States** 44 | Create snapshots of application states that can be backed up and later restored. 45 | 46 | - **Configuration Management in DevOps** 47 | Serialize configuration settings for software deployments, enabling easy versioning and rollback. 48 | 49 | - **Content Management Systems (CMS)** 50 | Serialize page or post states in CMS, enabling advanced versioning and preview functionalities. 51 | 52 | - **Customer Support Tools** 53 | Serialize user issues and their context, helping support teams to quickly understand and resolve customer problems. 54 | 55 | - **Data Migration Between Databases** 56 | Serialize entire database records for easy transfer between different database systems or formats. 57 | 58 | - **Educational Platforms** 59 | Serialize user progress and states in educational platforms, allowing students to pause and resume their learning activities. 60 | 61 | - **E-commerce Cart Persistence** 62 | Serialize shopping cart contents, enabling users to return to a filled cart even after their session expires. 63 | 64 | - **Energy Management Systems** 65 | Serialize energy usage data from various sensors for analysis and monitoring. 66 | 67 | - **Environmental Monitoring Systems** 68 | Serialize sensor data from environmental monitoring systems for analysis and historical record keeping. 69 | 70 | - **Event Sourcing in Applications** 71 | Use serialized states for event sourcing, maintaining an immutable log of state changes over time. 72 | 73 | - **Gaming State Preservation** 74 | Store game states as serialized strings, allowing players to resume games exactly where they left off. 75 | 76 | - **Healthcare Data Exchange** 77 | Securely transfer patient data between different healthcare systems while maintaining the integrity of complex data structures. 78 | 79 | - **IoT Device State Management** 80 | Serialize the state of IoT devices for efficient transmission over networks, aiding in remote monitoring and control. 81 | 82 | - **Legal Document Management** 83 | Serialize versions of legal documents, maintaining a trail of edits and changes for auditing. 84 | 85 | - **Machine Learning Data Preparation** 86 | Serialize complex data structures used in machine learning pipelines for efficient processing. 87 | 88 | - **Microservice Communication** 89 | Serialize complex objects for inter-service communication, ensuring efficient data transfer and reducing the need for complex parsing logic. 90 | 91 | - **Real Estate Portfolio Management** 92 | Serialize complex property data for portfolio management and analysis. 93 | 94 | - **Real-time Collaboration Tools** 95 | Serialize document or application states for real-time collaboration tools, ensuring consistency across different user sessions. 96 | 97 | - **Research Data Management** 98 | Serialize research data and experimental setups for ease of sharing and replication of experiments. 99 | 100 | - **Retail Inventory Management** 101 | Serialize inventory data, including details of products, for efficient management and tracking. 102 | 103 | - **Session Continuity Across Devices** 104 | Store user session data as a serialized string, enabling users to resume their session on a different device without loss of context. 105 | 106 | - **State Management for Single Page Applications (SPAs)** 107 | Serialize UI states into URL-safe strings, enabling bookmarking or sharing of specific application states. 108 | 109 | - **Supply Chain Logistics** 110 | Serialize complex logistics and shipment data, aiding in efficient tracking and management. 111 | 112 | - **Telecommunication Network Management** 113 | Serialize configurations and states of network devices for efficient management and troubleshooting. 114 | 115 | - **Travel Itinerary Planning** 116 | Serialize travel plans and itineraries, allowing users to save and share their travel details easily. 117 | 118 | - **Version Control of Design Files** 119 | Serialize design artifacts for version control in graphic design and CAD applications. 120 | -------------------------------------------------------------------------------- /lib/universalid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_support/all" 4 | require "zeitwerk" 5 | 6 | module UniversalID 7 | module Extensions; end 8 | 9 | class << self 10 | attr_writer :logger 11 | 12 | def logger 13 | @logger ||= defined?(Rails) ? Rails.logger : Logger.new(File::NULL) 14 | end 15 | end 16 | end 17 | 18 | Zeitwerk::Loader.for_gem(warn_on_extra_files: false).tap do |loader| 19 | loader.inflector = Zeitwerk::GemInflector.new(__dir__) 20 | loader.ignore "#{__dir__}/universalid/**/*message_pack_type*" 21 | loader.ignore "#{__dir__}/universalid/extensions" 22 | loader.inflector.inflect("universalid" => "UniversalID") 23 | loader.inflector.inflect("uri" => "URI") 24 | loader.inflector.inflect("uid" => "UID") 25 | loader.inflector.inflect("version" => "VERSION") 26 | loader.setup 27 | loader.eager_load 28 | end 29 | 30 | UniversalID::Settings.instance # initialize settings 31 | -------------------------------------------------------------------------------- /lib/universalid/encoder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "base64" 4 | require "brotli" 5 | 6 | # This module provides the ability to encode and decode objects into a compressed, URL-safe string 7 | module UniversalID::Encoder 8 | class << self 9 | def encode(object, options = {}) 10 | packed = UniversalID::Packer.pack(object, options) 11 | deflated = Brotli.deflate(packed) 12 | Base64.urlsafe_encode64 deflated, padding: false 13 | end 14 | 15 | def decode(string) 16 | decoded = Base64.urlsafe_decode64(string) 17 | inflated = Brotli.inflate(decoded) 18 | UniversalID::Packer.unpack inflated 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/universalid/extensions/active_record/base_message_pack_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined? ActiveRecord::Base 4 | 5 | require_relative "base_packer" 6 | require_relative "base_unpacker" 7 | 8 | UniversalID::MessagePackFactory.register( 9 | type: ActiveRecord::Base, 10 | recreate_pool: false, 11 | packer: ->(obj, packer) { UniversalID::Extensions::ActiveRecordBasePacker.new(obj).pack_with packer }, 12 | unpacker: ->(unpacker) { UniversalID::Extensions::ActiveRecordBaseUnpacker.unpack_with unpacker } 13 | ) 14 | 15 | end 16 | -------------------------------------------------------------------------------- /lib/universalid/extensions/active_record/base_packer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined? ActiveRecord 4 | 5 | class UniversalID::Extensions::ActiveRecordBasePacker 6 | using UniversalID::Refinements::HashRefinement 7 | 8 | # TODO: implement support for has_one 9 | # ActiveRecord::Reflection::HasOneReflection 10 | # 11 | # TODO: implement support for has_and_belongs_to_many 12 | # ActiveRecord::Reflection::HasAndBelongsToManyReflection 13 | # 14 | HAS_MANY_ASSOCIATIONS = [ 15 | ActiveRecord::Reflection::HasManyReflection 16 | ] 17 | 18 | DESCENDANTS_KEY = "uid:descendants" 19 | 20 | attr_reader :record 21 | 22 | def initialize(record) 23 | @record = record 24 | end 25 | 26 | def pack_with(packer) 27 | packer.write record.class.name 28 | packer.write packable_attributes 29 | end 30 | 31 | def prepack_options 32 | options = record.instance_variable_get(:@_uid_prepack_options) 33 | options = UniversalID::PrepackOptions.new unless options.is_a?(UniversalID::PrepackOptions) 34 | options 35 | end 36 | 37 | def prepack_database_options 38 | prepack_options.database_options 39 | end 40 | 41 | private 42 | 43 | def packable_attributes 44 | hash = if id_only? 45 | record.attributes.slice record.class.primary_key 46 | else 47 | record.attributes.select { |name, _| prepack_options.keep_key? name }.tap do |attrs| 48 | reject_keys! attrs if prepack_database_options.exclude_keys? 49 | reject_timestamps! attrs if prepack_database_options.exclude_timestamps? 50 | reject_unsaved_changes! attrs if prepack_database_options.exclude_changes? 51 | end 52 | end 53 | 54 | if include_descendants? 55 | add_descendants! hash 56 | hash.delete DESCENDANTS_KEY if hash[DESCENDANTS_KEY].empty? 57 | end 58 | 59 | hash["marked_for_destruction"] = true if record.marked_for_destruction? 60 | 61 | hash.prepack prepack_options 62 | end 63 | 64 | # helpers .................................................................................................. 65 | def id_only? 66 | return false if record.new_record? 67 | 68 | # explicit exclusion of primary key 69 | return false if prepack_options.reject_key?(record.class.primary_key) 70 | 71 | # explicit exclusion of all db keys and primary key is not explicitly included 72 | return false if prepack_database_options.exclude_keys? && !prepack_options.keep_key?(record.class.primary_key) 73 | 74 | # non-pk attribute names 75 | attribute_names = record.attributes.keys - [record.class.primary_key] 76 | 77 | # explicit inclusion of non-pk attributes 78 | return false if prepack_options.includes.any? && attribute_names.any? { |attr| prepack_options.includes[attr] } 79 | 80 | # record has unsaved non-pk changes and we want to keep them 81 | return false if prepack_database_options.include_changes? && attribute_names.any? { |attr| record.changes[attr] } 82 | 83 | prepack_database_options.include_keys? 84 | end 85 | 86 | def include_descendants? 87 | return false unless prepack_database_options.include_descendants? 88 | 89 | max_depth = prepack_database_options.descendant_depth.to_i 90 | record_depth = record.instance_variable_get(:@_uid_depth).to_i 91 | record_depth < max_depth 92 | end 93 | 94 | # attribute mutators ....................................................................................... 95 | 96 | def reject_keys!(hash) 97 | hash.delete record.class.primary_key unless prepack_options.includes[record.class.primary_key] 98 | foreign_key_column_names.each { |key| hash.delete(key) unless prepack_options.includes[key] } 99 | end 100 | 101 | def reject_timestamps!(hash) 102 | timestamp_column_names.each { |key| hash.delete key unless prepack_options.includes[key] } 103 | end 104 | 105 | def reject_unsaved_changes!(hash) 106 | record.changes_to_save.each do |key, (original_value, _)| 107 | hash[key] = original_value if prepack_options.keep_key?(key) 108 | end 109 | end 110 | 111 | def add_descendants!(hash) 112 | hash[DESCENDANTS_KEY] ||= {} 113 | 114 | has_many_descendant_instances_by_association_name.each do |name, relation| 115 | descendants = relation.each_with_object([]) do |descendant, memo| 116 | next unless descendant.persisted? || prepack_database_options.include_changes? 117 | 118 | descendant.instance_variable_set(:@_uid_depth, prepack_database_options.current_depth + 1) 119 | prepacked = UniversalID::Prepacker.prepack(descendant, prepack_options.to_h) 120 | memo << UniversalID::MessagePackFactory.msgpack_pool.dump(prepacked) 121 | ensure 122 | prepack_database_options.decrement_current_depth! 123 | descendant.remove_instance_variable :@_uid_depth 124 | end 125 | hash[DESCENDANTS_KEY][name.to_s] = descendants 126 | end 127 | 128 | prepack_database_options.increment_current_depth! 129 | end 130 | 131 | # active record helpers .................................................................................... 132 | 133 | def timestamp_column_names 134 | record.class.all_timestamp_attributes_in_model 135 | end 136 | 137 | def foreign_key_column_names 138 | record.class.reflections 139 | .each_with_object([]) do |(name, reflection), memo| 140 | memo << reflection.foreign_key if reflection.macro == :belongs_to 141 | end 142 | end 143 | 144 | def associations 145 | record.class.reflect_on_all_associations 146 | end 147 | 148 | def has_many_associations 149 | associations.select { |a| HAS_MANY_ASSOCIATIONS.include? a.class } 150 | end 151 | 152 | # Returns a has of the current in-memory `has_many` associated records keyed by name 153 | def has_many_descendant_instances_by_association_name 154 | has_many_associations.each_with_object({}) do |association, memo| 155 | relation = record.public_send(association.name) 156 | 157 | descendants = Set.new 158 | 159 | # persisted records 160 | relation.each { |descendant| descendants << descendant } if relation.loaded? 161 | 162 | # new records 163 | relation.target.each { |descendant| descendants << descendant } if relation.target.any? 164 | 165 | memo[association.name] = descendants.to_a if descendants.any? 166 | end 167 | end 168 | end 169 | 170 | end 171 | -------------------------------------------------------------------------------- /lib/universalid/extensions/active_record/base_unpacker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined? ActiveRecord 4 | 5 | class UniversalID::Extensions::ActiveRecordBaseUnpacker 6 | class << self 7 | def unpack_with(unpacker) 8 | class_name = unpacker.read 9 | attributes = unpacker.read || {} 10 | create_instance class_name, attributes 11 | end 12 | 13 | private 14 | 15 | def create_instance(class_name, attributes) 16 | klass = Object.const_get(class_name) if Object.const_defined?(class_name) 17 | return nil unless klass 18 | 19 | record = if attributes[klass.primary_key] 20 | klass.find_by(klass.primary_key => attributes[klass.primary_key]) 21 | end 22 | record ||= klass.new 23 | 24 | assign_attributes record, attributes.except("marked_for_destruction") 25 | record.mark_for_destruction if attributes["marked_for_destruction"] 26 | assign_descendants record, attributes 27 | 28 | record 29 | end 30 | 31 | def assign_attributes(record, attributes) 32 | attributes.each do |key, value| 33 | record.public_send :"#{key}=", value if record.respond_to? :"#{key}=" 34 | end 35 | end 36 | 37 | def assign_descendants(record, attributes) 38 | descendants = attributes[UniversalID::Extensions::ActiveRecordBasePacker::DESCENDANTS_KEY] || {} 39 | descendants.each do |name, list| 40 | next unless record.respond_to?(name) && record.respond_to?(:"#{name}=") 41 | 42 | models = list.map { |packed| UniversalID::MessagePackFactory.msgpack_pool.load(packed) } 43 | models.compact! 44 | next unless models.any? 45 | 46 | new_models = models.select(&:new_record?) 47 | models -= new_models 48 | association_collection = record.public_send(name) 49 | 50 | # restore persisted models 51 | # NOTE: ActiveRecord is smart enough to not re-create or re-add 52 | # existing records for has_many associations 53 | record.public_send :"#{name}=", models if models.any? 54 | 55 | # restore new unsaved models 56 | association_collection.target.concat new_models if new_models.any? 57 | 58 | # mark association relation as loaded 59 | association_collection.proxy_association.instance_variable_set :@loaded, true 60 | end 61 | end 62 | end 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /lib/universalid/extensions/active_record/relation_message_pack_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined? ActiveRecord::Relation 4 | 5 | # TODO: Revisit ActiveRecord::Relation serialization strategy, 6 | # and attempt to optimize without falling back to Marshal.dump/load 7 | UniversalID::MessagePackFactory.register( 8 | type: ActiveRecord::Relation, 9 | recreate_pool: false, 10 | packer: ->(obj, packer) do 11 | # NOTE: packing a relation will reset the loaded state and internal cache of the relation 12 | # this ensures minimal payload size 13 | obj = obj.dup # clear internal cached state (loaded results, etc.) 14 | obj.reset # dup should clear any internal caching, but we call reset just in case 15 | packer.write Marshal.dump(obj) 16 | end, 17 | unpacker: ->(unpacker) { Marshal.load unpacker.read } 18 | ) 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/universalid/extensions/active_support/cache/entry_message_pack_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined? ActiveSupport::Cache::Entry 4 | 5 | UniversalID::MessagePackFactory.register( 6 | type: ActiveSupport::Cache::Entry, 7 | packer: ->(obj, packer) do 8 | packer.write obj.value 9 | packer.write obj.version 10 | packer.write obj.expires_at 11 | end, 12 | unpacker: ->(unpacker) do 13 | value = unpacker.read 14 | version = unpacker.read 15 | expires_at = unpacker.read 16 | ActiveSupport::Cache::Entry.new value, version: version, expires_at: expires_at 17 | end 18 | ) 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/universalid/extensions/active_support/cache/store_message_pack_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined? ActiveSupport::Cache::Store 4 | 5 | UniversalID::MessagePackFactory.register( 6 | type: ActiveSupport::Cache::Store, 7 | packer: ->(obj, packer) do 8 | packer.write obj.class.name 9 | packer.write obj.options 10 | packer.write obj.instance_variable_get(:@data) 11 | end, 12 | unpacker: ->(unpacker) do 13 | class_name = unpacker.read 14 | options = unpacker.read 15 | data = unpacker.read 16 | Object.const_get(class_name).new(options).tap do |store| 17 | store.instance_variable_set :@data, data 18 | end 19 | end 20 | ) 21 | 22 | end 23 | -------------------------------------------------------------------------------- /lib/universalid/extensions/active_support/time_with_zone_message_pack_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined? ActiveSupport::TimeWithZone 4 | 5 | UniversalID::MessagePackFactory.register( 6 | type: ActiveSupport::TimeWithZone, 7 | packer: ->(obj, packer) do 8 | packer.write obj.to_time.utc 9 | packer.write obj.time_zone.tzinfo.identifier 10 | end, 11 | unpacker: ->(unpacker) do 12 | utc = unpacker.read 13 | tz = unpacker.read 14 | utc.in_time_zone ActiveSupport::TimeZone[tz] 15 | end 16 | ) 17 | 18 | end 19 | -------------------------------------------------------------------------------- /lib/universalid/extensions/global_id/global_id_model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined? GlobalID::Identification 4 | 5 | class UniversalID::Extensions::GlobalIDModel 6 | include GlobalID::Identification 7 | 8 | def self.find(value) 9 | new value 10 | end 11 | 12 | attr_reader :id, :uid 13 | 14 | def initialize(universal_id) 15 | @uid = case universal_id 16 | when URI::UID then universal_id 17 | when String then URI::UID.match?(universal_id) ? 18 | URI::UID.parse(universal_id) : 19 | URI::UID.from_payload(universal_id) 20 | end 21 | 22 | @id = uid&.payload 23 | end 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/universalid/extensions/global_id/global_id_uid_extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined?(GlobalID::Identification) && defined?(SignedGlobalID) 4 | 5 | require "forwardable" 6 | require_relative "global_id_model" 7 | 8 | module UniversalID::Extensions::GlobalIDUIDExtension 9 | extend Forwardable 10 | 11 | def self.included(mixer) 12 | mixer.extend ClassMethods 13 | end 14 | 15 | # Adds all GlobalID::Identification methods 16 | def_delegators(:to_global_id_model, *GlobalID::Identification.instance_methods(false)) 17 | 18 | # Returns a UniversalID::Extensions::GlobalIDModel instance 19 | # which implements the GlobalID::Identification interface/protocol 20 | def to_global_id_model 21 | UniversalID::Extensions::GlobalIDModel.new self 22 | end 23 | 24 | module ClassMethods 25 | def from_global_id_record(gid_record) 26 | gid_record&.find&.uid 27 | end 28 | 29 | def from_global_id(gid, options = {}) 30 | from_global_id_record GlobalID.parse(gid, options) 31 | end 32 | 33 | alias_method :from_gid, :from_global_id 34 | 35 | def from_signed_global_id(sgid, options = {}) 36 | from_global_id_record SignedGlobalID.parse(sgid, options) 37 | end 38 | 39 | alias_method :from_sgid, :from_signed_global_id 40 | end 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/universalid/extensions/global_id/message_pack_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined? GlobalID 4 | 5 | require_relative "global_id_uid_extension" 6 | 7 | URI::UID.include UniversalID::Extensions::GlobalIDUIDExtension 8 | 9 | UniversalID::MessagePackFactory.register( 10 | type: GlobalID, 11 | recreate_pool: false, 12 | packer: ->(obj, packer) { packer.write obj.to_param }, 13 | unpacker: ->(unpacker) { GlobalID.parse unpacker.read } 14 | ) 15 | 16 | end 17 | -------------------------------------------------------------------------------- /lib/universalid/extensions/signed_global_id/message_pack_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined? SignedGlobalID 4 | 5 | UniversalID::MessagePackFactory.register( 6 | type: SignedGlobalID, 7 | recreate_pool: false, 8 | packer: ->(obj, packer) { packer.write obj.to_param }, 9 | unpacker: ->(unpacker) { SignedGlobalID.parse unpacker.read } 10 | ) 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "etc" 4 | require "msgpack" 5 | 6 | UniversalID::MessagePackFactory = MessagePack::Factory.new.tap do |factory| 7 | class << factory 8 | attr_reader :msgpack_pool 9 | 10 | def create_msgpack_pool 11 | @msgpack_pool = UniversalID::MessagePackFactory.pool([Etc.nprocessors.to_i, 1].max) 12 | end 13 | 14 | def register_scalar(type:, recreate_pool: true, **options) 15 | register id: next_type_id(order: :asc), type: type, **options 16 | end 17 | 18 | def register(type:, id: nil, recreate_pool: true, **options) 19 | options[:recursive] = true unless options.key?(:recursive) 20 | register_type(id || next_type_id(order: :desc), type, options) 21 | create_msgpack_pool if recreate_pool 22 | end 23 | 24 | def next_type_id(order:) 25 | range = 0..127 26 | 27 | case order 28 | when :asc 29 | id = range.first 30 | id += 1 while type_registered?(id) 31 | when :desc 32 | id = range.last 33 | id -= 1 while type_registered?(id) 34 | end 35 | 36 | id = nil unless range.cover?(id) 37 | id 38 | end 39 | end 40 | end 41 | 42 | # Register MessagePack built-in types 43 | UniversalID::MessagePackFactory.register_type MessagePack::Timestamp::TYPE, ::Time, 44 | packer: MessagePack::Time::Packer, 45 | unpacker: MessagePack::Time::Unpacker 46 | 47 | # Register MessagePack built-in extensions 48 | UniversalID::MessagePackFactory.register_type 0x00, ::Symbol 49 | 50 | # Register UniversalID types/extensions 51 | require_relative "message_pack_types" 52 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # NOTE: MessagePack scans registered type in linear order and first match wins 4 | 5 | # scalars 6 | require_relative "message_pack_types/scalars/bigdecimal" 7 | require_relative "message_pack_types/scalars/complex" 8 | require_relative "message_pack_types/scalars/rational" 9 | require_relative "message_pack_types/scalars/date_time" 10 | require_relative "message_pack_types/scalars/date" 11 | require_relative "message_pack_types/scalars/range" 12 | require_relative "message_pack_types/scalars/regexp" 13 | 14 | # composites 15 | require_relative "message_pack_types/composites/module" 16 | require_relative "message_pack_types/composites/open_struct" 17 | require_relative "message_pack_types/composites/struct" 18 | require_relative "message_pack_types/composites/set" 19 | 20 | # extensions 21 | Dir["#{__dir__}/extensions/**/*.rb"].sort.each { |f| require f } 22 | 23 | UniversalID::MessagePackFactory.create_msgpack_pool 24 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types/composites/module.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | UniversalID::MessagePackFactory.register( 4 | type: Module, 5 | recreate_pool: false, 6 | packer: ->(obj, packer) { packer.write obj.name }, 7 | unpacker: ->(unpacker) do 8 | name = unpacker.read 9 | Object.const_defined?(name) ? Object.const_get(name) : nil 10 | end 11 | ) 12 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types/composites/open_struct.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "ostruct" 4 | 5 | UniversalID::MessagePackFactory.register( 6 | type: OpenStruct, 7 | recreate_pool: false, 8 | packer: ->(obj, packer) { packer.write obj.to_h }, 9 | unpacker: ->(unpacker) { OpenStruct.new unpacker.read } 10 | ) 11 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types/composites/set.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | UniversalID::MessagePackFactory.register( 4 | type: Set, 5 | recusive: true, 6 | recreate_pool: false, 7 | packer: ->(obj, packer) { packer.write obj.to_a }, 8 | unpacker: ->(unpacker) { Set.new unpacker.read } 9 | ) 10 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types/composites/struct.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | UniversalID::MessagePackFactory.register( 4 | type: Struct, 5 | recreate_pool: false, 6 | packer: ->(obj, packer) do 7 | packer.write obj.class.name 8 | packer.write obj.to_h 9 | end, 10 | 11 | unpacker: ->(unpacker) do 12 | class_name = unpacker.read 13 | hash = unpacker.read 14 | klass = Object.const_get(class_name) if class_name && Object.const_defined?(class_name) 15 | klass ||= Struct.new(*hash.keys) 16 | 17 | if klass 18 | # shenanigans to support ::Ruby 3.0.X and 3.1.X 19 | RUBY_VERSION.start_with?("3.0", "3.1") ? 20 | klass.new.tap { |struct| hash.each { |key, val| struct[key] = hash[key] } } : 21 | klass.new(**hash) 22 | end 23 | end 24 | ) 25 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types/scalars/bigdecimal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bigdecimal" 4 | 5 | UniversalID::MessagePackFactory.register_scalar( 6 | type: BigDecimal, 7 | recreate_pool: false, 8 | packer: ->(obj, packer) { packer.write obj.to_s }, 9 | unpacker: ->(unpacker) { BigDecimal unpacker.read } 10 | ) 11 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types/scalars/complex.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | UniversalID::MessagePackFactory.register_scalar( 4 | type: Complex, 5 | recreate_pool: false, 6 | packer: ->(obj, packer) { packer.write obj.to_s }, 7 | unpacker: ->(unpacker) { Kernel.Complex unpacker.read } 8 | ) 9 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types/scalars/date.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "date" 4 | 5 | UniversalID::MessagePackFactory.register_scalar( 6 | type: Date, 7 | recreate_pool: false, 8 | packer: ->(obj, packer) { packer.write obj.iso8601 }, 9 | unpacker: ->(unpacker) { Date.parse unpacker.read } 10 | ) 11 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types/scalars/date_time.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "date" 4 | 5 | UniversalID::MessagePackFactory.register_scalar( 6 | type: DateTime, 7 | recreate_pool: false, 8 | packer: ->(obj, packer) { packer.write obj.iso8601(9) }, 9 | unpacker: ->(unpacker) { DateTime.parse unpacker.read } 10 | ) 11 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types/scalars/range.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | UniversalID::MessagePackFactory.register_scalar( 4 | type: Range, 5 | recreate_pool: false, 6 | packer: ->(obj, packer) do 7 | packer.write obj.first 8 | packer.write obj.to_s.scan(/\.{2,3}/).first 9 | packer.write obj.last 10 | end, 11 | unpacker: ->(unpacker) do 12 | first = unpacker.read 13 | operator = unpacker.read 14 | last = unpacker.read 15 | case operator 16 | when ".." then first..last 17 | when "..." then first...last 18 | end 19 | end 20 | ) 21 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types/scalars/rational.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | UniversalID::MessagePackFactory.register_scalar( 4 | type: Rational, 5 | recreate_pool: false, 6 | packer: ->(obj, packer) { packer.write obj.to_s }, 7 | unpacker: ->(unpacker) { Kernel.Rational unpacker.read } 8 | ) 9 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types/scalars/regexp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | UniversalID::MessagePackFactory.register_scalar( 4 | type: Regexp, 5 | recreate_pool: false, 6 | packer: ->(obj, packer) do 7 | packer.write obj.source 8 | packer.write obj.options 9 | end, 10 | unpacker: ->(unpacker) do 11 | source = unpacker.read 12 | options = unpacker.read 13 | Regexp.new source, options 14 | end 15 | ) 16 | -------------------------------------------------------------------------------- /lib/universalid/message_pack_types/uri/uid/type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | UniversalID::MessagePackFactory.register( 4 | type: URI::UID, 5 | recreate_pool: false, 6 | packer: ->(obj, packer) { packer.write obj.to_s }, 7 | unpacker: ->(unpacker) { URI::UID.parse unpacker.read } 8 | ) 9 | -------------------------------------------------------------------------------- /lib/universalid/packer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer 4 | class << self 5 | def pack(object, options = {}) 6 | object = UniversalID::Prepacker.prepack(object, options) 7 | 8 | # This is basically the same call as UniversalID::MessagePackFactory.pack(object), 9 | # but it uses a pool of pre-initialized packers/unpackers instead of creating a new one each time 10 | UniversalID::MessagePackFactory.msgpack_pool.dump object 11 | end 12 | 13 | def unpack(string) 14 | # This is basically the same call as UniversalID::MessagePackFactory.unpack(object), 15 | # but it uses a pool of pre-initialized packers/unpackers instead of creating a new one each time 16 | UniversalID::MessagePackFactory.msgpack_pool.load string 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/universalid/prepack_database_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::PrepackDatabaseOptions 4 | def initialize(settings) 5 | @settings = settings 6 | end 7 | 8 | def to_h 9 | @settings.to_h 10 | end 11 | 12 | def include_keys? 13 | !!@settings.include_keys 14 | end 15 | 16 | def exclude_keys? 17 | !include_keys? 18 | end 19 | 20 | def include_timestamps? 21 | !!@settings.include_timestamps 22 | end 23 | 24 | def exclude_timestamps? 25 | !include_timestamps? 26 | end 27 | 28 | def include_changes? 29 | !!@settings.include_changes 30 | end 31 | 32 | def exclude_changes? 33 | !include_changes? 34 | end 35 | 36 | def descendant_depth 37 | @settings.descendant_depth ||= 0 38 | end 39 | 40 | attr_writer :current_depth 41 | 42 | def current_depth 43 | @settings.current_depth ||= 0 44 | end 45 | 46 | def increment_current_depth! 47 | @settings.current_depth ||= 0 48 | @settings.current_depth = @settings.current_depth += 1 49 | end 50 | 51 | def decrement_current_depth! 52 | @settings.current_depth ||= 0 53 | @settings.current_depth = @settings.current_depth -= 1 54 | end 55 | 56 | def include_descendants? 57 | !!@settings.include_descendants 58 | end 59 | 60 | def exclude_descentants? 61 | !include_descendants? 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/universalid/prepack_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::PrepackOptions 4 | attr_reader :excludes, :includes, :database_options 5 | 6 | def initialize(options = {}) 7 | options ||= {} 8 | @settings = UniversalID::Settings.build(**options).prepack 9 | @database_options = UniversalID::PrepackDatabaseOptions.new(@settings.database) 10 | @references = Set.new 11 | @excludes ||= @settings.exclude.to_h { |key| [key.to_s, true] } 12 | @includes ||= @settings.include.to_h { |key| [key.to_s, true] } 13 | end 14 | 15 | def to_h 16 | @settings.to_h 17 | end 18 | 19 | def prevent_self_reference!(object) 20 | raise UniversalID::Prepacker::CircularReferenceError if @references.include?(object.object_id) 21 | @references << object.object_id 22 | end 23 | 24 | def include_blank? 25 | !!@settings.include_blank 26 | end 27 | 28 | def exclude_blank? 29 | !include_blank? 30 | end 31 | 32 | def keep_key?(key) 33 | return false if excludes[key.to_s] 34 | includes.none? || includes[key.to_s] 35 | end 36 | 37 | def reject_key?(key) 38 | !!excludes[key.to_s] 39 | end 40 | 41 | def keep_value?(value) 42 | include_blank? || present?(value) 43 | end 44 | 45 | def reject_value?(value) 46 | !keep_value?(value) 47 | end 48 | 49 | def keep_keypair?(key, value) 50 | keep_key?(key) && keep_value?(value) 51 | end 52 | 53 | def reject_keypair?(key, value) 54 | reject_key?(key) || reject_value?(value) 55 | end 56 | 57 | def blank?(value) 58 | (value == false) ? false : value.blank? 59 | end 60 | 61 | def present?(value) 62 | !blank?(value) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/universalid/prepacker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Prepacker 4 | using UniversalID::Refinements::ArrayRefinement 5 | using UniversalID::Refinements::HashRefinement 6 | using UniversalID::Refinements::SetRefinement 7 | using UniversalID::Refinements::OpenStructRefinement 8 | 9 | class CircularReferenceError < StandardError 10 | def initialize(message = "Prepacking not supported on self referencing objects!") 11 | super 12 | end 13 | end 14 | 15 | class << self 16 | def prepack(object, options = {}) 17 | options = UniversalID::PrepackOptions.new(options) unless options.is_a?(UniversalID::PrepackOptions) 18 | object.instance_variable_set(:@_uid_prepack_options, options) unless object.frozen? 19 | 20 | return object unless object.respond_to?(:prepack) 21 | 22 | object.prepack options 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/universalid/refinements.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module UniversalID::Refinements; end 4 | -------------------------------------------------------------------------------- /lib/universalid/refinements/array_refinement.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module UniversalID::Refinements::ArrayRefinement 4 | refine Array do 5 | def prepack(options) 6 | options.prevent_self_reference! self 7 | 8 | copy = each_with_object([]) do |val, memo| 9 | val = UniversalID::Prepacker.prepack(val, options) 10 | memo << val if options.keep_value?(val) 11 | end 12 | 13 | copy.compact! if options.exclude_blank? 14 | copy 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/universalid/refinements/hash_refinement.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module UniversalID::Refinements::HashRefinement 4 | refine Hash do 5 | using UniversalID::Refinements::ArrayRefinement 6 | 7 | def prepack(options) 8 | options.prevent_self_reference! self 9 | 10 | copy = each_with_object({}) do |(key, val), memo| 11 | next unless options.keep_keypair?(key, val) 12 | memo[key] = UniversalID::Prepacker.prepack(val, options) 13 | end 14 | 15 | copy.compact! if options.exclude_blank? 16 | copy 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/universalid/refinements/open_struct_refinement.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "ostruct" 4 | 5 | module UniversalID::Refinements::OpenStructRefinement 6 | refine OpenStruct do 7 | using UniversalID::Refinements::HashRefinement 8 | 9 | def prepack(options) 10 | options.prevent_self_reference! self 11 | OpenStruct.new to_h.prepack(options) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/universalid/refinements/set_refinement.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module UniversalID::Refinements::SetRefinement 4 | refine Set do 5 | using UniversalID::Refinements::ArrayRefinement 6 | 7 | def prepack(options) 8 | options.prevent_self_reference! self 9 | Set.new to_a.prepack(options) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/universalid/settings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "monitor" 4 | require "singleton" 5 | require "config" 6 | 7 | class UniversalID::Settings 8 | include MonitorMixin 9 | include Singleton 10 | 11 | DEFAULT_FILE_PATH = File.expand_path("../../config/default.yml", __dir__) 12 | 13 | class << self 14 | def build(**options) 15 | instance.default_copy.tap do |settings| 16 | options.each { |key, val| assign key, val, to: settings } 17 | end 18 | end 19 | 20 | def register(...) 21 | instance.register(...) 22 | end 23 | 24 | def [](key) 25 | instance[key] 26 | end 27 | 28 | private 29 | 30 | def assign(key, value, to:) 31 | return if value.nil? 32 | 33 | case {key.to_sym => value} 34 | in prepack: prepack then prepack.each { |k, v| assign k, v, to: to } 35 | in exclude: exclude then to.prepack.exclude = exclude 36 | in include: inc then to.prepack.include = inc 37 | in include_blank: include_blank then to.prepack.include_blank = !!include_blank 38 | in database: database then database.each { |k, v| assign k, v, to: to } 39 | in include_keys: include_keys then to.prepack.database.include_keys = !!include_keys 40 | in include_timestamps: include_timestamps then to.prepack.database.include_timestamps = !!include_timestamps 41 | in include_changes: include_changes then to.prepack.database.include_changes = !!include_changes 42 | in include_unsaved_changes: include_changes then to.prepack.database.include_changes = !!include_changes # TODO: Remove in v1.0 43 | in include_descendants: include_descendants then to.prepack.database.include_descendants = !!include_descendants 44 | in descendant_depth: descendant_depth then to.prepack.database.descendant_depth = descendant_depth 45 | else # ignore key 46 | end 47 | end 48 | end 49 | 50 | def register(key, options = {}) 51 | key = key.to_s.strip.downcase.to_sym 52 | synchronize do 53 | raise ArgumentError, "Already registered! key: #{key}" if registry.key? key 54 | config = case options 55 | when String then Config.load_files(options) 56 | when Hash then Config::Options.new(options) 57 | when Config::Options then options 58 | else raise ArgumentError, "Invalid options! Must be a String, Hash, or Config::Options." 59 | end 60 | 61 | config = self.class.build(**config) unless key == :default 62 | registry[key] = config 63 | self.class.define_method(key) { config } 64 | self.class.define_method(:"#{key}_copy") { Marshal.load Marshal.dump(config) } 65 | self.class.define_singleton_method(key) { instance.public_send key } 66 | [key, config] 67 | end 68 | end 69 | 70 | def [](key) 71 | registry[key.to_sym] 72 | end 73 | 74 | private 75 | 76 | attr_reader :registry 77 | 78 | def initialize 79 | super 80 | @registry = {} 81 | register :default, DEFAULT_FILE_PATH 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/universalid/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module UniversalID 4 | VERSION = "0.1.7" 5 | end 6 | -------------------------------------------------------------------------------- /lib/uri/uid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "uri" 4 | 5 | unless defined?(::URI::UID) || ::URI.scheme_list.include?("UID") 6 | 7 | module URI 8 | class UID < ::URI::Generic 9 | VERSION = UniversalID::VERSION 10 | SCHEME = "uid" 11 | HOST = "universalid" 12 | PATTERN = /\A#{SCHEME}:\/\/#{HOST}\/[-_0-9A-Z]+#[-_0-9A-Z]+\z/io 13 | 14 | class << self 15 | def encoder 16 | UniversalID::Encoder 17 | end 18 | 19 | def fingerprint(object) 20 | encode fingerprint_components(object) 21 | end 22 | 23 | def parse(value) 24 | return nil if value.nil? 25 | return value if value.is_a?(self) 26 | 27 | value = value.to_s 28 | return nil if value.strip.empty? 29 | 30 | new(*::URI.split(value)) 31 | end 32 | 33 | def match?(uri) 34 | return true if uri.is_a?(self) 35 | uri.to_s.match? PATTERN 36 | end 37 | 38 | def build_string(payload, object = nil) 39 | "#{SCHEME}://#{HOST}/#{payload}##{fingerprint(object)}" 40 | end 41 | 42 | def build(object, options = {}, &block) 43 | path = "/#{encode(object, options, &block)}" 44 | parse "#{SCHEME}://#{HOST}#{path}##{fingerprint(object)}" 45 | end 46 | 47 | def from_payload(payload, object = nil) 48 | parse(build_string(payload, object)).tap do |uid| 49 | # NOTE: fingerprint mismatch can happen when building from a UID payload 50 | # ensure the fingerprint is correct 51 | if uid&.valid? && URI::UID.fingerprint(uid.decode) != uid.fingerprint 52 | remove_instance_variable :@decoded_fingerprint if instance_variable_defined?(:@decoded_fingerprint) 53 | uid.instance_variable_set :@fragment, URI::UID.build(uid.decode).fingerprint 54 | end 55 | end 56 | end 57 | 58 | def encode(object, options = {}) 59 | return yield(object, options) if block_given? 60 | encoder.encode object, options 61 | end 62 | 63 | def decode(...) 64 | encoder.decode(...) 65 | end 66 | 67 | # Creates a new URI::UID with the given URI components. 68 | # SEE: https://ruby-doc.org/3.2.2/stdlibs/uri/URI/Generic.html#method-c-new 69 | # 70 | # @param scheme [String] the scheme component. 71 | # @param userinfo [String] the userinfo component. 72 | # @param host [String] the host component. 73 | # @param port [Integer] the port component. 74 | # @param registry [String] the registry component. 75 | # @param path [String] the path component. 76 | # @param opaque [String] the opaque component. 77 | # @param query [String] the query component. 78 | # @param fragment [String] the fragment component. 79 | # @param parser [URI::Parser] the parser to use for the URI, defaults to DEFAULT_PARSER. 80 | # @param arg_check [Boolean] whether to check arguments, defaults to false. 81 | # @return [URI::UID] the new URI::UID instance. 82 | # # @raise [URI::InvalidURIError] if the URI is malformed. 83 | # @raise [ArgumentError] if the number of arguments is incorrect or an argument is of the wrong type. 84 | # @raise [TypeError] if an argument is not of the expected type. 85 | # @raise [URI::InvalidComponentError] if a component of the URI is not valid. 86 | # @raise [URI::BadURIError] if the URI is in a bad or unexpected state. 87 | def new(...) 88 | super.tap do |uri| 89 | if uri.invalid? 90 | raise ::URI::InvalidComponentError, "Scheme must be `#{SCHEME}`" if uri.scheme != SCHEME 91 | raise ::URI::InvalidComponentError, "Host must be `#{HOST}`" if uri.host != HOST 92 | raise ::URI::InvalidComponentError, "Unable to parse `payload` from the path component!" if uri.payload.strip.empty? 93 | end 94 | end 95 | end 96 | 97 | private 98 | 99 | def fingerprint_components(object) 100 | klass = object.is_a?(Class) ? object : object.class 101 | tokens = [klass] 102 | 103 | begin 104 | path = const_source_location(klass.name).first.to_s 105 | tokens << ::File.mtime(path).utc if ::File.exist?(path) 106 | rescue => e 107 | UniversalID.logger&.warn "URI::UID#fingerprint: Unable to determine the source location for #{klass.name}!\n#{e.message}}" 108 | end 109 | 110 | tokens 111 | end 112 | end 113 | 114 | def payload 115 | path[1..] 116 | end 117 | 118 | def fingerprint(decode: false) 119 | return @decoded_fingerprint ||= decode_fingerprint if decode 120 | fragment 121 | end 122 | 123 | def valid? 124 | case self 125 | in scheme: SCHEME, host: HOST, path: p, fragment: _ if p.size >= 8 then true 126 | else false 127 | end 128 | end 129 | 130 | def invalid? 131 | !valid? 132 | end 133 | 134 | def decode(force: false) 135 | return nil unless valid? 136 | 137 | remove_instance_variable :@decoded if force && instance_variable_defined?(:@decoded) 138 | return @decoded if defined?(@decoded) 139 | 140 | @decoded ||= yield(decode_payload, *decode_fingerprint) if block_given? 141 | @decoded ||= decode_payload 142 | end 143 | 144 | def deconstruct_keys(_keys) 145 | {scheme: scheme, host: host, path: path, fragment: fragment} 146 | end 147 | 148 | def inspect 149 | "#" 150 | end 151 | 152 | private 153 | 154 | def decode_payload 155 | self.class.decode payload 156 | end 157 | 158 | def decode_fingerprint 159 | self.class.decode fingerprint 160 | end 161 | end 162 | 163 | # Register the URI scheme 164 | if ::URI.respond_to? :register_scheme 165 | ::URI.register_scheme "UID", UID unless ::URI.scheme_list.include?("UID") 166 | else 167 | # shenanigans to support Ruby 3.0.X 168 | ::URI::UID = UID unless defined?(::URI::UID) 169 | ::URI.scheme_list["UID"] = UID 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /test/rails_kit/models/active_record_etl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "oj" 4 | 5 | module ActiveRecordETL 6 | class << self 7 | def included(klass) 8 | klass.define_method(:etl_pipeline) { @etl_pipeline ||= ActiveRecordETL::Pipeline.new(self) } 9 | klass.delegate :extract, :transform, :load, to: :etl_pipeline 10 | end 11 | 12 | # Parses a ActiveRecordETL transformed payload in the specified format 13 | # 14 | # @param payload [String] The tranformed payload to parse 15 | # @param :format [Symbol] The data format to transform the record into (optional, defaults to :json) 16 | # @return [Object] the parsed payload 17 | # @raise [NotImplementedError] if the specified format is not supported 18 | def parse(payload, format: :json) 19 | case format 20 | # when :json then JSON.parse payload 21 | when :json then Oj.load payload 22 | else raise NotImplementedError 23 | end 24 | end 25 | end 26 | 27 | class Pipeline 28 | # Initializes a new ActiveRecord ETL Data Pipeline 29 | # @param record [ActiveRecord] the record to ETL 30 | def initialize(record) 31 | @record = record 32 | end 33 | 34 | # The wrapped ActiveRecord instance 35 | attr_reader :record 36 | 37 | # The record's attributes 38 | # 39 | # @return [Hash] the record's attributes 40 | def attributes 41 | record.attributes 42 | end 43 | 44 | # Returns a list of all the record's associations 45 | # 46 | # @param :macro [String, Symbol] (:belongs_to, :has_many, ... optional, defaults to nil) 47 | # @return [Array] 48 | def associations(macro: nil) 49 | list = record.class.reflect_on_all_associations 50 | list = list.select { |a| a.macro == macro.to_sym } if macro 51 | list 52 | end 53 | 54 | # Returns a the record's loaded associations by name 55 | # 56 | # @param :macro [String] (:belongs_to, :has_many, ... optional, defaults to nil) 57 | # @return [Hash{String => ActiveRecord::Associations::CollectionProxy}] 58 | def loaded_associations_by_name(macro: nil) 59 | associations(macro: macro).each_with_object({}) do |association, memo| 60 | collection_proxy = record.public_send(association.name) 61 | memo[association.name.to_s] = collection_proxy if collection_proxy.loaded? 62 | end 63 | end 64 | 65 | # Returns the record's loaded `has_many` associations by name 66 | # 67 | # @return [Hash{String => ActiveRecord::Associations::CollectionProxy}] 68 | def loaded_has_many_associations_by_name 69 | loaded_associations_by_name macro: :has_many 70 | end 71 | 72 | # The record's primary key name 73 | # 74 | # @return [String] 75 | def primary_key 76 | record.class.primary_key 77 | end 78 | 79 | # Attribute names that the record `accepts_nested_attributes_for` 80 | # 81 | # @return [Array] 82 | def nested_attribute_names 83 | record.class.nested_attributes_options.keys.map(&:to_s) 84 | end 85 | 86 | # Attribute names that the record `accepts_nested_attributes_for` that have been loaded into memory 87 | # 88 | # @return [Array] 89 | def loaded_nested_attribute_names 90 | nested_attribute_names & loaded_has_many_associations_by_name.keys 91 | end 92 | 93 | # Attribute names for all the record's `belongs_to` associations 94 | # 95 | # @return [Array] 96 | def parent_attribute_names 97 | record.class.reflections.each_with_object([]) do |(name, reflection), memo| 98 | memo << reflection.foreign_key if reflection.macro == :belongs_to 99 | end 100 | end 101 | 102 | # Attribute names for the record's timestamps 103 | # 104 | # @return [Array] 105 | def timestamp_attribute_names 106 | record.class.all_timestamp_attributes_in_model 107 | end 108 | 109 | # Extracts data from the record 110 | # 111 | # @param :except [Array] List of attributes to omit (optional, trumps :only, defaults to []) 112 | # @param :only [Array] List of attributes to extract (optional, defaults to []) 113 | # @param :copy [Boolean] Whether or not to omit keys and timestamps (optional, defaults to false) 114 | # @param :nested_attributes [Boolean] Indicates if nested attributes should be included (optional, defaults to false) 115 | # @param :reject_blank [Boolean] Indicates if blank values should be omitted (optional, defaults to false) 116 | # @return [Hash{String => Object}] The extracted data 117 | def extract(**options) 118 | options = normalize_options(**options) 119 | 120 | hash = attributes.each_with_object({}) do |(name, value), memo| 121 | memo[name] = value unless skip?(name, value, **options) 122 | end 123 | 124 | if options[:nested_attributes] 125 | loaded_nested_attribute_names.each do |name| 126 | key = "#{name}_attributes" 127 | values = record.send(name) 128 | hash[key] = values.map { |val| extract_next(val, **options) } unless skip?(name, values, **options) 129 | end 130 | end 131 | 132 | hash 133 | end 134 | 135 | # Transforms the record into the specified data format 136 | # 137 | # @param :format [Symbol] The data format to transform the record into (optional, defaults to :json) 138 | # @param :except [Array] List of attributes to omit (optional, trumps :only, defaults to []) 139 | # @param :only [Array] List of attributes to extract (optional, defaults to []) 140 | # @param :copy [Boolean] Whether or not to omit keys and timestamps (optional, defaults to false) 141 | # @param :nested_attributes [Boolean] Indicates if nested attributes should be included (optional, defaults to false) 142 | # @param :reject_blank [Boolean] Indicates if blank values should be omitted (optional, defaults to false) 143 | # @return [String] the transformed data 144 | # @raise [NotImplementedError] if the specified format is not supported 145 | def transform(format: :json, **options) 146 | case format 147 | # when :json then extract(**options).to_json 148 | when :json then Oj.dump extract(**options), symbol_keys: false 149 | else raise NotImplementedError 150 | end 151 | end 152 | 153 | def load 154 | raise NotImplementedError 155 | end 156 | 157 | private 158 | 159 | def extract_next(record, **options) 160 | self.class.new(record).extract(**options) 161 | end 162 | 163 | def normalize_only_values(**options) 164 | (options[:only] || []).map(&:to_s) 165 | end 166 | 167 | def normalize_except_values(**options) 168 | (options[:except] || []).map(&:to_s).tap do |except| 169 | if options[:copy] 170 | except << primary_key 171 | except.concat parent_attribute_names, timestamp_attribute_names 172 | end 173 | end 174 | end 175 | 176 | def normalize_options(**options) 177 | options[:only] = normalize_only_values(**options) 178 | options[:except] = normalize_except_values(**options) 179 | options 180 | end 181 | 182 | def skip?(name, value, **options) 183 | name = name.to_s 184 | return true if options[:except].any? && options[:except].include?(name) 185 | return true if options[:only].any? && options[:only].exclude?(name) 186 | return true if value.blank? && options[:reject_blank] 187 | false 188 | end 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /test/rails_kit/models/active_record_forge.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | require "active_record" 5 | require "faker" 6 | 7 | module ActiveRecordForge 8 | def self.included(klass) 9 | klass.define_singleton_method(:foundry) { @foundry ||= ActiveRecordForge::Foundry.new(self) } 10 | klass.define_singleton_method(:forge) { |*args, **kwargs| foundry.forge(*args, **kwargs) } 11 | klass.define_singleton_method(:forge!) { |*args, **kwargs| foundry.forge!(*args, **kwargs) } 12 | klass.define_singleton_method(:generate_attributes) { foundry.generate_attributes } 13 | end 14 | 15 | class Foundry 16 | extend Forwardable 17 | 18 | attr_reader :klass 19 | 20 | def initialize(klass) 21 | @klass = klass 22 | end 23 | 24 | def forge(count = 1, **options) 25 | options = options.stringify_keys 26 | attribute_options = options.slice(*column_names) 27 | association_options = options.slice(*association_names) 28 | 29 | records = count.times.map { klass.build generate_attributes.merge(attribute_options) } 30 | 31 | records.each_with_index do |record, i| 32 | association_options.each do |association_name, association_count| 33 | association_count.times do 34 | association_record = association(association_name).klass.forge(**options) 35 | record.public_send(association_name) << association_record 36 | end 37 | end 38 | end 39 | 40 | (records.size == 1) ? records.first : records 41 | end 42 | 43 | def forge!(...) 44 | forge(...).tap { |forged| forged.is_a?(Array) ? forged.each(&:save!) : forged.save! } 45 | end 46 | 47 | def generate_attributes 48 | attributes = {} 49 | 50 | columns.each do |column| 51 | next if column.default 52 | next if column.name == primary_key 53 | next if foreign_key_column_names.include?(column.name) 54 | 55 | case column.type 56 | when :string 57 | options = [ 58 | Faker::Commerce.product_name, 59 | Faker::Company.bs, 60 | Faker::Company.catch_phrase, 61 | Faker::Company.department, 62 | Faker::Company.industry, 63 | Faker::Company.name, 64 | Faker::Marketing.buzzwords 65 | ] 66 | attributes[column.name] = options.sample(rand(1..2)).join(" ") 67 | when :text 68 | options = [ 69 | Faker::Movies::Hackers.quote, 70 | Faker::Movies::Lebowski.quote, 71 | Faker::Movies::PrincessBride.quote, 72 | Faker::Movies::StarWars.quote, 73 | Faker::TvShows::MichaelScott.quote, 74 | Faker::TvShows::RickAndMorty.quote, 75 | Faker::TvShows::SiliconValley.quote 76 | ] 77 | attributes[column.name] = options.sample(rand(2..5)).join(" ") 78 | end 79 | end 80 | 81 | attributes.with_indifferent_access 82 | end 83 | 84 | private 85 | 86 | def_delegators :klass, :columns, :column_names, :primary_key, :reflections, :reflect_on_all_associations 87 | 88 | def association(name) 89 | associations.find { |a| a.name.to_s == name.to_s } 90 | end 91 | 92 | def associations(macro: nil) 93 | list = reflect_on_all_associations 94 | list = list.select { |a| a.macro == macro.to_sym } if macro 95 | list 96 | end 97 | 98 | def association_names(macro: nil) 99 | associations(macro: macro).map { |a| a.name.to_s } 100 | end 101 | 102 | def foreign_key_column_names 103 | reflections.each_with_object([]) do |(name, reflection), memo| 104 | memo << reflection.foreign_key if reflection.macro == :belongs_to 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /test/rails_kit/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "model_probe" 4 | require_relative "active_record_etl" 5 | require_relative "active_record_forge" 6 | require_relative "../../../lib/universalid/version" 7 | 8 | class ApplicationRecord < ActiveRecord::Base 9 | extend ModelProbe 10 | 11 | include ActiveRecordETL 12 | include ActiveRecordForge 13 | include GlobalID::Identification 14 | 15 | VERSION = UniversalID::VERSION 16 | 17 | self.abstract_class = true 18 | end 19 | -------------------------------------------------------------------------------- /test/rails_kit/models/attachment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Attachment < ApplicationRecord 4 | belongs_to :email 5 | end 6 | -------------------------------------------------------------------------------- /test/rails_kit/models/campaign.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Campaign < ApplicationRecord 4 | has_many :emails, dependent: :destroy 5 | accepts_nested_attributes_for :emails 6 | 7 | scope :email_subjects_like, ->(value) do 8 | where id: Email.subject_like(value).select(:campaign_id) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/rails_kit/models/email.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Email < ApplicationRecord 4 | belongs_to :campaign 5 | has_many :attachments, dependent: :destroy 6 | accepts_nested_attributes_for :attachments 7 | 8 | scope :subject_like, ->(subject) do 9 | where(arel_table[:subject].matches("%#{subject}%")) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/rails_kit/setup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record" 4 | require "globalid" 5 | require "rails" 6 | require "stringio" 7 | 8 | GlobalID.app = SignedGlobalID.app = "universal-id" 9 | SignedGlobalID.verifier = GlobalID::Verifier.new("4ae705a3f0f0c675236cc7067d49123d") 10 | 11 | begin 12 | stdout = $stdout 13 | $stdout = StringIO.new 14 | 15 | ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") 16 | 17 | ActiveRecord::Schema.define do 18 | create_table :campaigns do |t| 19 | t.column :name, :string 20 | t.column :description, :text 21 | t.column :trigger, :string 22 | t.timestamps 23 | end 24 | 25 | create_table :emails do |t| 26 | t.column :campaign_id, :integer 27 | t.column :subject, :string 28 | t.column :body, :text 29 | t.column :wait, :integer 30 | t.timestamps 31 | end 32 | 33 | create_table :attachments do |t| 34 | t.column :email_id, :integer 35 | t.column :file_name, :string 36 | t.column :content_type, :string 37 | t.column :file_size, :integer 38 | t.column :file_data, :binary 39 | t.timestamps 40 | end 41 | end 42 | ensure 43 | $stdout = stdout 44 | end 45 | 46 | require_relative "models/application_record" 47 | require_relative "models/campaign" 48 | require_relative "models/email" 49 | require_relative "models/attachment" 50 | 51 | # Seed some data 52 | # ActiveRecord::Base.logger = Logger.new(STDOUT) 53 | Campaign.forge! 10, emails: 5, attachments: 3 54 | -------------------------------------------------------------------------------- /test/test_extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record" 4 | require "amazing_print" 5 | require "benchmark" 6 | require "bigdecimal" 7 | require "date" 8 | require "etc" 9 | require "faker" 10 | require "globalid" 11 | require "minitest/autorun" 12 | require "minitest/parallel" 13 | require "minitest/reporters" 14 | require "rails" 15 | require "rainbow" 16 | require "simplecov" 17 | require "timecop" 18 | 19 | # MiniTest setup 20 | Minitest.parallel_executor = Minitest::Parallel::Executor.new([Etc.nprocessors, 1].max) # thread count 21 | Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new 22 | 23 | # TODO: Get coverage working 24 | # Coverage setup 25 | # SimpleCov.start do 26 | # project_name "UniversalID" 27 | # add_filter [ 28 | # "lib/universalid/message_pack_types", # ....................coverage doesn't work on these 29 | # "lib/universalid/extensions/**/*message_pack_type.rb", # ...coverage doesn't work on these 30 | # "lib/universalid/refinements", # ...........................coverage doesn't work on these 31 | # "test" # ...................................................coverage not wanted 32 | # ] 33 | # end 34 | 35 | require "universalid" 36 | 37 | # Minimal subset of Rails tooling for testing purposes 38 | require_relative "rails_kit/setup" 39 | 40 | module UniversalID::MessagePack; end 41 | 42 | class Minitest::Test 43 | alias_method :original_run, :run 44 | 45 | def run 46 | Campaign.destroy_all 47 | result = nil 48 | time = Benchmark.measure { result = original_run } 49 | time = time.real.round(5) 50 | time_in_ms = time * 1000 51 | 52 | message = " #{Rainbow("⬇").dimgray.faint} " 53 | message << Rainbow("Benchmark ").dimgray 54 | message << if time >= 0.03 55 | Rainbow("> 30ms ").red.faint + Rainbow("(#{"%1.3f sec" % time})").red + Rainbow("(#{"%6.2fms" % time_in_ms})").red.bright 56 | elsif time > 0.01 57 | Rainbow("< 30ms ").yellow.faint + Rainbow("(#{"%1.3f sec" % time})").yellow + Rainbow("(#{"%6.2fms" % time_in_ms})").yellow.bright 58 | else 59 | Rainbow("< 10ms ").green.faint + Rainbow("(#{"%1.3f sec" % time})").green + Rainbow("(#{"%6.2f ms" % time_in_ms})").green.bright 60 | end 61 | message << Rainbow("".ljust(55, ".")).dimgray.faint 62 | puts message 63 | 64 | result 65 | end 66 | 67 | def load_has_many(record, depth: 0) 68 | count = 0 69 | with_has_many record do |relation| 70 | next unless count < depth 71 | count += 1 72 | relation.load 73 | relation.each { |rec| load_has_many(rec, depth: depth - 1) } 74 | end 75 | end 76 | 77 | def with_has_many(record) 78 | record.class.reflect_on_all_associations.each do |association| 79 | next unless association.is_a?(ActiveRecord::Reflection::HasManyReflection) 80 | relation = record.public_send(association.name) 81 | yield relation 82 | end 83 | end 84 | 85 | def assert_has_many_loaded(record, depth: 1) 86 | count = 0 87 | with_has_many record do |relation| 88 | next unless count <= depth 89 | count += 1 90 | assert relation.loaded? 91 | relation.each { |rec| assert_has_many_loaded(rec, depth: depth - 1) } 92 | end 93 | end 94 | 95 | def refute_has_many_loaded(record) 96 | with_has_many record do |relation| 97 | refute relation.loaded? 98 | end 99 | end 100 | 101 | def assert_has_many(expected, actual) 102 | assert_equal expected.size, actual.size 103 | expected.each_with_index { |record, i| assert_record record, actual[i] } 104 | end 105 | 106 | def assert_record(expected, actual) 107 | assert_equal expected.attributes, actual.attributes 108 | assert_equal expected.persisted?, actual.persisted? 109 | assert_equal expected.changed?, actual.changed? 110 | 111 | with_has_many expected do |relation| 112 | expected_relation = relation 113 | actual_relation = actual.public_send(expected_relation.proxy_association.reflection.name) 114 | assert_equal expected_relation.size, actual_relation.size 115 | next unless expected_relation.loaded? 116 | next unless actual_relation.loaded? 117 | 118 | expected_relation.each_with_index do |record, i| 119 | assert_record record, actual_relation[i] 120 | end 121 | end 122 | end 123 | 124 | def assert_active_support_cache_store(expected, actual, data: scalars, expires_in: nil) 125 | refute data.blank? 126 | 127 | data.keys.each do |key| 128 | case key 129 | when :nil_class 130 | assert_nil expected.read(key) 131 | assert_nil actual.read(key) 132 | else 133 | assert_equal expected.read(key), actual.read(key) 134 | end 135 | end 136 | 137 | sleep expires_in.to_f 138 | 139 | data.keys.each do |key| 140 | assert_nil expected.read(key) 141 | assert_nil actual.read(key) 142 | end 143 | end 144 | 145 | def self.scalars 146 | @scalars ||= { 147 | bigdecimal: BigDecimal("123.45"), 148 | complex: Complex(1, 2), 149 | date: Date.today, 150 | datetime: DateTime.now, 151 | false_class: false, 152 | float: 123.45, 153 | integer: 123, 154 | nil_class: nil, 155 | range: 1..100, 156 | rational: Rational(3, 4), 157 | regexp: /abc/, 158 | string: "hello", 159 | symbol: :symbol, 160 | time: Time.now, 161 | true_class: true 162 | } 163 | end 164 | 165 | def scalars 166 | self.class.scalars 167 | end 168 | 169 | def time_with_zones 170 | month = ActiveSupport::TimeZone["UTC"].now.beginning_of_year 171 | 172 | Timecop.freeze time do 173 | 11.times do |i| 174 | ActiveSupport::TimeZone.all.each do |time_zone| 175 | time = month.in_time_zone(time_zone).advance(days: rand(1..28), minutes: rand(1..59)) 176 | yield time 177 | end 178 | 179 | month.advance months: 1 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /test/universalid/campaign_demo_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # class UniversalID::CampaignDemoTest < Minitest::Test 4 | # def test_campaign_demo 5 | # # ........................................................................................................ 6 | # # Create a Campaign via multi-step form (wizard) running over HTTP 7 | # # ........................................................................................................ 8 | # 9 | # # Step 1. Assign basic campaign info 10 | # campaign = Campaign.new(name: "Example Campaign", description: "Example Description") 11 | # param = campaign.to_packable.to_gid_param 12 | # 13 | # # Step 2. Create first email 14 | # campaign = Campaign.from_packable(param) 15 | # campaign.emails << campaign.emails.build(subject: "First Email", body: "Welcome", wait: 1.day) 16 | # param = campaign.to_packable(methods: :emails_attributes).to_gid_param 17 | # 18 | # # Step 3. Create second email 19 | # campaign = Campaign.from_packable(param) 20 | # campaign.emails << campaign.emails.build(subject: "Second Email", body: "Follow Up", wait: 1.week) 21 | # param = campaign.to_packable(methods: :emails_attributes).to_gid_param 22 | # 23 | # # Step 4. Create third email 24 | # campaign = Campaign.from_packable(param) 25 | # campaign.emails << campaign.emails.build(subject: "Third Email", body: "Hard Sell", wait: 2.days) 26 | # param = campaign.to_packable(methods: :emails_attributes).to_gid_param 27 | # 28 | # # Step 5. Configure final details 29 | # campaign = Campaign.from_packable(param) 30 | # campaign.assign_attributes trigger: "Sign Up" 31 | # param = campaign.to_packable(methods: :emails_attributes).to_gid_param 32 | # 33 | # # Step 6. Review and save 34 | # campaign = Campaign.from_packable(param) 35 | # campaign.save! 36 | # 37 | # # ........................................................................................................ 38 | # # Create a digital product from the Campaign (i.e. template) 39 | # # ........................................................................................................ 40 | # 41 | # # 1. Create a packable digital product from the Campaign ................................................. 42 | # # NOTE: The signed param is a sellable digital product with built in purpose and scarcity! 43 | # campaign = Campaign.first 44 | # packable = debug do 45 | # campaign 46 | # .to_packable(except: [:id, :campaign_id, :created_at, :updated_at], methods: :emails_attributes) 47 | # .to_sgid_param(for: "Promotion 123", expires_in: 30.seconds) 48 | # end 49 | # 50 | # # 2. Reconstruct the shared template (digital product) ................................................... 51 | # copy = Campaign.from_packable(signed_param, for: "Promotion 123") 52 | # 53 | # # 3. Let the product expire (wait 30 seconds) ............................................................ 54 | # # gid = UniversalID::PackableHash.parse_gid(signed_param, for: "Promotion 123") 55 | # invalid_copy = Campaign.from_packable(signed_param, for: "Promotion 123") 56 | # end 57 | # end 58 | -------------------------------------------------------------------------------- /test/universalid/encoder/active_record_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Encoder::ActiveRecordTest < Minitest::Test 4 | def test_new_model_exclude_changes 5 | campaign = Campaign.forge 6 | encoded = UniversalID::Encoder.encode(campaign) 7 | decoded = UniversalID::Encoder.decode(encoded) 8 | assert_equal campaign.class, decoded.class 9 | refute_equal campaign.attributes, decoded.attributes 10 | assert_empty decoded.attributes.compact 11 | end 12 | 13 | def test_new_model_include_changes 14 | campaign = Campaign.forge 15 | encoded = UniversalID::Encoder.encode(campaign, include_changes: true) 16 | decoded = UniversalID::Encoder.decode(encoded) 17 | assert_equal campaign.class, decoded.class 18 | assert_equal campaign.attributes, decoded.attributes 19 | end 20 | 21 | def test_new_model_include_changes_exclude_blanks 22 | campaign = Campaign.forge 23 | encoded = UniversalID::Encoder.encode(campaign, include_changes: true, include_blank: false) 24 | decoded = UniversalID::Encoder.decode(encoded) 25 | assert_equal campaign.class, decoded.class 26 | assert_equal campaign.attributes, decoded.attributes 27 | end 28 | 29 | def test_persisted_model 30 | campaign = Campaign.forge! 31 | encoded = UniversalID::Encoder.encode(campaign) 32 | decoded = UniversalID::Encoder.decode(encoded) 33 | assert_equal campaign, decoded 34 | end 35 | 36 | def test_persisted_model_marked_for_destruction 37 | campaign = Campaign.forge! 38 | campaign.mark_for_destruction 39 | encoded = UniversalID::Encoder.encode(campaign) 40 | decoded = UniversalID::Encoder.decode(encoded) 41 | assert_equal campaign, decoded 42 | assert decoded.marked_for_destruction? 43 | end 44 | 45 | def test_changed_persisted_model 46 | campaign = Campaign.forge! 47 | campaign.description = "Changed Description" 48 | encoded = UniversalID::Encoder.encode(campaign) 49 | decoded = UniversalID::Encoder.decode(encoded) 50 | assert_equal campaign, decoded 51 | refute_equal campaign.description, decoded.description 52 | end 53 | 54 | def test_changed_persisted_model_include_changes 55 | campaign = Campaign.forge! 56 | campaign.description = "Changed Description" 57 | encoded = UniversalID::Encoder.encode(campaign, include_changes: true) 58 | decoded = UniversalID::Encoder.decode(encoded) 59 | assert_equal campaign, decoded 60 | assert_equal campaign.description, decoded.description 61 | end 62 | 63 | def test_changed_persisted_model_with_registered_custom_config 64 | yaml = <<~YAML 65 | prepack: 66 | exclude: 67 | - description 68 | - trigger 69 | include_blank: false 70 | 71 | database: 72 | include_changes: true 73 | include_timestamps: false 74 | YAML 75 | 76 | _, settings = UniversalID::Settings.register("test_#{SecureRandom.alphanumeric(8)}", YAML.safe_load(yaml)) 77 | 78 | campaign = Campaign.forge! 79 | # remember orig values 80 | description = campaign.description 81 | trigger = campaign.trigger 82 | 83 | # change values 84 | campaign.name = "Changed Name" 85 | campaign.description = "Changed Description" 86 | campaign.trigger = "Changed Trigger" 87 | 88 | encoded = UniversalID::Encoder.encode(campaign, **settings) 89 | decoded = UniversalID::Encoder.decode(encoded) 90 | 91 | # same record 92 | assert_equal campaign, decoded 93 | 94 | # included values match 95 | assert_equal campaign.name, decoded.name 96 | 97 | # excluded values do not match the in-memory changes 98 | refute_equal campaign.description, decoded.description 99 | refute_equal campaign.trigger, decoded.trigger 100 | 101 | # excluded values match the original values 102 | assert_equal description, decoded.description 103 | assert_equal trigger, decoded.trigger 104 | end 105 | 106 | def test_persisted_model_deep_copy_with_unsaved_descendants 107 | campaign = Campaign.forge! 108 | emails = 3.times.map { |i| campaign.emails.build subject: "Unsaved Email: #{i}" } 109 | emails.each do |email| 110 | 2.times { email.attachments.build file_name: "Unsaved Attachment: #{email.subject}" } 111 | end 112 | 113 | options = { 114 | include_changes: true, 115 | include_descendants: true, 116 | descendant_depth: 2 117 | } 118 | 119 | encoded = UniversalID::Encoder.encode(campaign, **options) 120 | decoded = UniversalID::Encoder.decode(encoded) 121 | 122 | assert_equal campaign, decoded 123 | 124 | emails.each do |email| 125 | decoded_email = decoded.emails.find { |e| e.subject == email.subject } 126 | assert decoded_email 127 | 128 | email.attachments.each do |attachment| 129 | decoded_attachment = decoded_email.attachments.find { |a| a.file_name == attachment.file_name } 130 | assert decoded_attachment 131 | end 132 | end 133 | end 134 | 135 | def test_persisted_model_deep_copy_customized 136 | campaign = Campaign.forge! emails: 3, attachments: 2 137 | 138 | options = { 139 | include_blank: false, 140 | exclude: [:description, :body, :file_data], 141 | include_keys: false, 142 | include_timestamps: false, 143 | include_changes: false, 144 | include_descendants: true, 145 | descendant_depth: 2 146 | } 147 | 148 | encoded = UniversalID::Encoder.encode(campaign, options) 149 | decoded = UniversalID::Encoder.decode(encoded) 150 | 151 | # verify decoded records also have changes 152 | assert decoded.changed? 153 | assert decoded.emails.map(&:changed?) 154 | assert decoded.emails.map(&:attachments).flatten.map(&:changed?) 155 | 156 | # verify that the in-memory and decoded records are different 157 | refute_equal campaign, decoded 158 | refute_equal campaign.emails, decoded.emails 159 | refute_equal campaign.emails.map(&:attachments), decoded.emails.map(&:attachments) 160 | 161 | # verify that the in-memory decoded records do not have keys or excluded fields 162 | assert_nil decoded.id 163 | assert_nil decoded.description 164 | decoded.emails.each do |email| 165 | assert_nil email.id 166 | assert_nil email.campaign_id 167 | assert_nil email.body 168 | email.attachments.each do |attachment| 169 | assert_nil attachment.id 170 | assert_nil attachment.email_id 171 | assert_nil attachment.file_data 172 | end 173 | end 174 | 175 | # verify that we can save the new records 176 | assert decoded.save 177 | assert decoded.emails.map(&:persisted?) 178 | assert decoded.emails.map(&:attachments).flatten.map(&:persisted?) 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /test/universalid/encoder/ruby_composites_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Encoder::RubyCompositesTest < Minitest::Test 4 | SCALARS = { 5 | complex: Complex(1, 2), 6 | date: Date.today, 7 | datetime: DateTime.now, 8 | false_class: false, 9 | float: 123.45, 10 | integer: 123, 11 | nil_class: nil, 12 | range: 1..100, 13 | rational: Rational(3, 4), 14 | regexp: /abc/, 15 | string: "hello", 16 | symbol: :symbol, 17 | time: Time.now, 18 | true_class: true 19 | } 20 | 21 | NamedStruct = Struct.new(*SCALARS.keys) 22 | 23 | COMPOSITES = { 24 | Array => SCALARS.values, 25 | Hash => SCALARS, 26 | OpenStruct => OpenStruct.new(SCALARS), 27 | Set => Set.new(SCALARS.values), 28 | Struct => NamedStruct.new(*SCALARS.values) 29 | } 30 | 31 | COMPOSITES.each do |klass, value| 32 | define_method :"test_#{klass.name}" do 33 | value = COMPOSITES[klass] 34 | encoded = UniversalID::Encoder.encode(value) 35 | decoded = UniversalID::Encoder.decode(encoded) 36 | assert_equal value, decoded 37 | end 38 | 39 | # We don't really need to test prepack here but doing so led to 40 | # creating more refinemenments to expand prepack coverage 41 | if klass != Struct 42 | define_method :"test_#{klass.name}_with_prepack" do 43 | value = COMPOSITES[klass] 44 | encoded = UniversalID::Encoder.encode(value) 45 | decoded = UniversalID::Encoder.decode(encoded) 46 | assert_equal value, decoded 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/universalid/encoder/ruby_scalars_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Encoder::ScalarsTest < Minitest::Test 4 | SCALARS = { 5 | Complex => Complex(1, 2), 6 | Date => Date.today, 7 | DateTime => DateTime.now, 8 | FalseClass => false, 9 | Float => 123.45, 10 | Integer => 123, 11 | NilClass => nil, 12 | Range => 1..100, 13 | Rational => Rational(3, 4), 14 | Regexp => /abc/, 15 | String => "hello", 16 | Symbol => :symbol, 17 | Time => Time.now, 18 | TrueClass => true 19 | } 20 | 21 | SCALARS.each do |klass, value| 22 | define_method :"test_#{klass.name}" do 23 | value = SCALARS[klass] 24 | encoded = UniversalID::Encoder.encode(value) 25 | decoded = UniversalID::Encoder.decode(encoded) 26 | 27 | if value.nil? 28 | assert_nil decoded 29 | else 30 | assert_equal value, decoded 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/universalid/extensions/active_record/active_record_associations_changed_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::ActiveRecordAssociationsChangedTest < Minitest::Test 4 | def apply_changes(campaign) 5 | campaign.assign_attributes(Campaign.generate_attributes) 6 | campaign.emails.each do |email| 7 | email.assign_attributes(Email.generate_attributes) 8 | email.attachments.each do |attachment| 9 | attachment.assign_attributes(Attachment.generate_attributes) 10 | end 11 | end 12 | end 13 | 14 | def test_pack_unpack 15 | expected = Campaign.forge! emails: 3, attachments: 2 16 | load_has_many expected, depth: 2 17 | apply_changes expected 18 | 19 | packed = UniversalID::Packer.pack(expected, include_changes: true, include_descendants: true, descendant_depth: 2) 20 | actual = UniversalID::Packer.unpack(packed) 21 | 22 | assert_has_many_loaded expected, depth: 2 23 | assert expected.persisted? 24 | assert expected.changed? 25 | assert expected.emails do |email| 26 | assert email.changed? 27 | email.attachments { |attachment| assert attachment.changed? } 28 | end 29 | assert_has_many_loaded actual, depth: 2 30 | assert_record expected, actual 31 | end 32 | end 33 | 34 | class UniversalID::Encoder::ActiveRecordAssociationsChangedTest < Minitest::Test 35 | def apply_changes(campaign) 36 | campaign.assign_attributes(Campaign.generate_attributes) 37 | campaign.emails.each do |email| 38 | email.assign_attributes(Email.generate_attributes) 39 | email.attachments.each do |attachment| 40 | attachment.assign_attributes(Attachment.generate_attributes) 41 | end 42 | end 43 | end 44 | 45 | def test_encode_decode 46 | expected = Campaign.forge! emails: 3, attachments: 2 47 | load_has_many expected, depth: 2 48 | apply_changes expected 49 | 50 | encoded = UniversalID::Encoder.encode(expected, include_changes: true, include_descendants: true, descendant_depth: 2) 51 | actual = UniversalID::Encoder.decode(encoded) 52 | 53 | assert_has_many_loaded expected, depth: 2 54 | assert expected.persisted? 55 | assert expected.changed? 56 | assert expected.emails do |email| 57 | assert email.changed? 58 | email.attachments { |attachment| assert attachment.changed? } 59 | end 60 | assert_has_many_loaded actual, depth: 2 61 | assert_record expected, actual 62 | end 63 | end 64 | 65 | class URI::UID::ActiveRecordAssociationsChangedTest < Minitest::Test 66 | def apply_changes(campaign) 67 | campaign.assign_attributes(Campaign.generate_attributes) 68 | campaign.emails.each do |email| 69 | email.assign_attributes(Email.generate_attributes) 70 | email.attachments.each do |attachment| 71 | attachment.assign_attributes(Attachment.generate_attributes) 72 | end 73 | end 74 | end 75 | 76 | def test_build_parse_decode 77 | expected = Campaign.forge! emails: 3, attachments: 2 78 | load_has_many expected, depth: 2 79 | apply_changes expected 80 | 81 | uri = URI::UID.build(expected, include_changes: true, include_descendants: true, descendant_depth: 2).to_s 82 | uid = URI::UID.parse(uri) 83 | actual = uid.decode 84 | 85 | assert_has_many_loaded expected, depth: 2 86 | assert expected.persisted? 87 | assert expected.changed? 88 | assert expected.emails do |email| 89 | assert email.changed? 90 | email.attachments { |attachment| assert attachment.changed? } 91 | end 92 | assert_has_many_loaded actual, depth: 2 93 | assert_record expected, actual 94 | end 95 | 96 | def test_global_id 97 | expected = Campaign.forge! emails: 3, attachments: 2 98 | load_has_many expected, depth: 2 99 | apply_changes expected 100 | 101 | gid = URI::UID.build(expected, include_changes: true, include_descendants: true, descendant_depth: 2).to_gid_param 102 | uid = URI::UID.from_gid(gid) 103 | actual = uid.decode 104 | 105 | assert_has_many_loaded expected, depth: 2 106 | assert expected.persisted? 107 | assert expected.changed? 108 | assert expected.emails do |email| 109 | assert email.changed? 110 | email.attachments { |attachment| assert attachment.changed? } 111 | end 112 | assert_has_many_loaded actual, depth: 2 113 | assert_record expected, actual 114 | end 115 | 116 | def test_signed_global_id 117 | expected = Campaign.forge! emails: 3, attachments: 2 118 | load_has_many expected, depth: 2 119 | apply_changes expected 120 | 121 | sgid = URI::UID.build(expected, include_changes: true, include_descendants: true, descendant_depth: 2).to_sgid_param 122 | uid = URI::UID.from_sgid(sgid) 123 | actual = uid.decode 124 | 125 | assert_has_many_loaded expected, depth: 2 126 | assert expected.persisted? 127 | assert expected.changed? 128 | assert expected.emails do |email| 129 | assert email.changed? 130 | email.attachments { |attachment| assert attachment.changed? } 131 | end 132 | assert_has_many_loaded actual, depth: 2 133 | assert_record expected, actual 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /test/universalid/extensions/active_record/active_record_associations_persisted_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::ActiveRecordAssociationsPersistedTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = Campaign.forge! emails: 3, attachments: 2 6 | load_has_many expected, depth: 2 7 | 8 | packed = UniversalID::Packer.pack(expected) 9 | actual = UniversalID::Packer.unpack(packed) 10 | 11 | assert_has_many_loaded expected, depth: 2 12 | assert expected.persisted? 13 | refute_has_many_loaded actual 14 | assert_record expected, actual 15 | end 16 | end 17 | 18 | class UniversalID::Encoder::ActiveRecordAssociationsPersistedTest < Minitest::Test 19 | def test_encode_decode 20 | expected = Campaign.forge! emails: 3, attachments: 2 21 | load_has_many expected, depth: 2 22 | 23 | encoded = UniversalID::Encoder.encode(expected) 24 | actual = UniversalID::Encoder.decode(encoded) 25 | 26 | assert_has_many_loaded expected, depth: 2 27 | assert expected.persisted? 28 | refute_has_many_loaded actual 29 | assert_record expected, actual 30 | end 31 | end 32 | 33 | class URI::UID::ActiveRecordAssociationsPersistedTest < Minitest::Test 34 | def test_build_parse_decode 35 | expected = Campaign.forge! emails: 3, attachments: 2 36 | load_has_many expected, depth: 2 37 | 38 | uri = URI::UID.build(expected).to_s 39 | uid = URI::UID.parse(uri) 40 | actual = uid.decode 41 | 42 | assert_has_many_loaded expected, depth: 2 43 | assert expected.persisted? 44 | refute_has_many_loaded actual 45 | assert_record expected, actual 46 | end 47 | 48 | def test_global_id 49 | expected = Campaign.forge! emails: 3, attachments: 2 50 | load_has_many expected, depth: 2 51 | 52 | gid = URI::UID.build(expected).to_gid_param 53 | uid = URI::UID.from_gid(gid) 54 | actual = uid.decode 55 | 56 | assert_has_many_loaded expected, depth: 2 57 | assert expected.persisted? 58 | refute_has_many_loaded actual 59 | assert_record expected, actual 60 | end 61 | 62 | def test_signed_global_id 63 | expected = Campaign.forge! emails: 3, attachments: 2 64 | load_has_many expected, depth: 2 65 | 66 | sgid = URI::UID.build(expected).to_sgid_param 67 | uid = URI::UID.from_sgid(sgid) 68 | actual = uid.decode 69 | 70 | assert_has_many_loaded expected, depth: 2 71 | assert expected.persisted? 72 | refute_has_many_loaded actual 73 | assert_record expected, actual 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/universalid/extensions/active_record/active_record_associations_unpersisted_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::ActiveRecordAssociationsUnpersistedTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = Campaign.forge emails: 3, attachments: 2 6 | 7 | packed = UniversalID::Packer.pack(expected, include_changes: true, include_descendants: true, descendant_depth: 2) 8 | actual = UniversalID::Packer.unpack(packed) 9 | 10 | assert_has_many_loaded expected, depth: 2 11 | assert expected.new_record? 12 | assert_has_many_loaded actual, depth: 2 13 | assert_record expected, actual 14 | end 15 | end 16 | 17 | class UniversalID::Encoder::ActiveRecordAssociationsUnpersistedTest < Minitest::Test 18 | def test_encode_decode 19 | expected = Campaign.forge emails: 3, attachments: 2 20 | 21 | encoded = UniversalID::Encoder.encode(expected, include_changes: true, include_descendants: true, descendant_depth: 2) 22 | actual = UniversalID::Encoder.decode(encoded) 23 | 24 | assert_has_many_loaded expected, depth: 2 25 | assert expected.new_record? 26 | assert_has_many_loaded actual, depth: 2 27 | assert_record expected, actual 28 | end 29 | end 30 | 31 | class URI::UID::ActiveRecordAssociationsUnpersistedTest < Minitest::Test 32 | def test_build_parse_decode 33 | expected = Campaign.forge emails: 3, attachments: 2 34 | 35 | uri = URI::UID.build(expected, include_changes: true, include_descendants: true, descendant_depth: 2).to_s 36 | uid = URI::UID.parse(uri) 37 | actual = uid.decode 38 | 39 | assert_has_many_loaded expected, depth: 2 40 | assert expected.new_record? 41 | assert_has_many_loaded actual, depth: 2 42 | assert_record expected, actual 43 | end 44 | 45 | def test_global_id 46 | expected = Campaign.forge emails: 3, attachments: 2 47 | 48 | gid = URI::UID.build(expected, include_changes: true, include_descendants: true, descendant_depth: 2).to_gid_param 49 | uid = URI::UID.from_gid(gid) 50 | actual = uid.decode 51 | 52 | assert_has_many_loaded expected, depth: 2 53 | assert expected.new_record? 54 | assert_has_many_loaded actual, depth: 2 55 | assert_record expected, actual 56 | end 57 | 58 | def test_signed_global_id 59 | expected = Campaign.forge emails: 3, attachments: 2 60 | 61 | sgid = URI::UID.build(expected, include_changes: true, include_descendants: true, descendant_depth: 2).to_sgid_param 62 | uid = URI::UID.from_sgid(sgid) 63 | actual = uid.decode 64 | 65 | assert_has_many_loaded expected, depth: 2 66 | assert expected.new_record? 67 | assert_has_many_loaded actual, depth: 2 68 | assert_record expected, actual 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/universalid/extensions/active_record/active_record_changed_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::ActiveRecordChangedTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = Campaign.forge! 6 | expected.assign_attributes(Campaign.generate_attributes) 7 | 8 | assert expected.changed? 9 | 10 | packed = UniversalID::Packer.pack(expected, include_changes: true) 11 | actual = UniversalID::Packer.unpack(packed) 12 | 13 | assert_equal expected.attributes, actual.attributes 14 | end 15 | end 16 | 17 | class UniversalID::Encoder::ActiveRecordChangedTest < Minitest::Test 18 | def test_encode_decode 19 | expected = Campaign.forge! 20 | expected.assign_attributes(Campaign.generate_attributes) 21 | 22 | assert expected.changed? 23 | 24 | encoded = UniversalID::Encoder.encode(expected, include_changes: true) 25 | actual = UniversalID::Encoder.decode(encoded) 26 | 27 | assert_equal expected.attributes, actual.attributes 28 | end 29 | end 30 | 31 | class URI::UID::ActiveRecordChangedTest < Minitest::Test 32 | def test_build_parse_decode 33 | expected = Campaign.forge! 34 | expected.assign_attributes(Campaign.generate_attributes) 35 | 36 | assert expected.changed? 37 | 38 | uri = URI::UID.build(expected, include_changes: true).to_s 39 | uid = URI::UID.parse(uri) 40 | actual = uid.decode 41 | 42 | assert_equal expected.attributes, actual.attributes 43 | end 44 | 45 | def test_global_id 46 | expected = Campaign.forge! 47 | expected.assign_attributes(Campaign.generate_attributes) 48 | 49 | assert expected.changed? 50 | 51 | gid = URI::UID.build(expected, include_changes: true).to_gid_param 52 | uid = URI::UID.from_gid(gid) 53 | actual = uid.decode 54 | 55 | assert_equal expected.attributes, actual.attributes 56 | end 57 | 58 | def test_signed_global_id 59 | expected = Campaign.forge! 60 | expected.assign_attributes(Campaign.generate_attributes) 61 | 62 | assert expected.changed? 63 | 64 | sgid = URI::UID.build(expected, include_changes: true).to_sgid_param 65 | uid = URI::UID.from_sgid(sgid) 66 | actual = uid.decode 67 | 68 | assert_equal expected.attributes, actual.attributes 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/universalid/extensions/active_record/active_record_persisted_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::ActiveRecordPersistedTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = Campaign.forge! 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal expected, actual 10 | end 11 | end 12 | 13 | class UniversalID::Encoder::ActiveRecordPersistedTest < Minitest::Test 14 | def test_encode_decode 15 | expected = Campaign.forge! 16 | encoded = UniversalID::Encoder.encode(expected) 17 | actual = UniversalID::Encoder.decode(encoded) 18 | 19 | assert_equal expected, actual 20 | end 21 | end 22 | 23 | class URI::UID::ActiveRecordPersistedTest < Minitest::Test 24 | def test_build_parse_decode 25 | expected = Campaign.forge! 26 | uri = URI::UID.build(expected).to_s 27 | uid = URI::UID.parse(uri) 28 | actual = uid.decode 29 | 30 | assert_equal expected, actual 31 | end 32 | 33 | def test_global_id 34 | expected = Campaign.forge! 35 | gid = URI::UID.build(expected).to_gid_param 36 | uid = URI::UID.from_gid(gid) 37 | actual = uid.decode 38 | 39 | assert_equal expected, actual 40 | end 41 | 42 | def test_signed_global_id 43 | expected = Campaign.forge! 44 | sgid = URI::UID.build(expected).to_sgid_param 45 | uid = URI::UID.from_sgid(sgid) 46 | actual = uid.decode 47 | 48 | assert_equal expected, actual 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/universalid/extensions/active_record/active_record_relation_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::ActiveRecordRelationTest < Minitest::Test 4 | def test_pack_unpack 5 | Campaign.forge! 5, emails: 3, subject: "Flash Sale 123" 6 | expected = Campaign.email_subjects_like("Sale") 7 | expected.load 8 | expected.map { |c| c.emails.load } 9 | 10 | assert_equal 5, Campaign.count 11 | assert_equal 15, Email.count 12 | assert expected.loaded? 13 | assert expected.map { |c| c.emails.loaded? }.any? 14 | 15 | packed = UniversalID::Packer.pack(expected) 16 | actual = UniversalID::Packer.unpack(packed) 17 | 18 | refute actual.loaded? 19 | refute actual.map { |c| c.emails.loaded? }.any? 20 | assert_equal expected, actual 21 | assert_equal expected.to_a, actual.to_a 22 | end 23 | end 24 | 25 | class UniversalID::Encoder::ActiveRecordRelationTest < Minitest::Test 26 | def test_encode_decode 27 | Campaign.forge! 5, emails: 3, subject: "Flash Sale 123" 28 | expected = Campaign.email_subjects_like("Sale") 29 | expected.load 30 | expected.map { |c| c.emails.load } 31 | 32 | assert_equal 5, Campaign.count 33 | assert_equal 15, Email.count 34 | assert expected.loaded? 35 | assert expected.map { |c| c.emails.loaded? }.any? 36 | 37 | encoded = UniversalID::Encoder.encode(expected) 38 | actual = UniversalID::Encoder.decode(encoded) 39 | 40 | refute actual.loaded? 41 | refute actual.map { |c| c.emails.loaded? }.any? 42 | assert_equal expected, actual 43 | assert_equal expected.to_a, actual.to_a 44 | end 45 | end 46 | 47 | class URI::UID::ActiveRecordRelationTest < Minitest::Test 48 | def test_build_parse_decode 49 | Campaign.forge! 5, emails: 3, subject: "Flash Sale 123" 50 | expected = Campaign.email_subjects_like("Sale") 51 | expected.load 52 | expected.map { |c| c.emails.load } 53 | 54 | assert_equal 5, Campaign.count 55 | assert_equal 15, Email.count 56 | assert expected.loaded? 57 | assert expected.map { |c| c.emails.loaded? }.any? 58 | 59 | uri = URI::UID.build(expected).to_s 60 | uid = URI::UID.parse(uri) 61 | actual = uid.decode 62 | 63 | refute actual.loaded? 64 | refute actual.map { |c| c.emails.loaded? }.any? 65 | assert_equal expected, actual 66 | assert_equal expected.to_a, actual.to_a 67 | end 68 | 69 | def test_global_id 70 | Campaign.forge! 5, emails: 3, subject: "Flash Sale 123" 71 | expected = Campaign.email_subjects_like("Sale") 72 | expected.load 73 | expected.map { |c| c.emails.load } 74 | 75 | assert_equal 5, Campaign.count 76 | assert_equal 15, Email.count 77 | assert expected.loaded? 78 | assert expected.map { |c| c.emails.loaded? }.any? 79 | 80 | gid = URI::UID.build(expected).to_gid_param 81 | uid = URI::UID.from_gid(gid) 82 | actual = uid.decode 83 | 84 | refute actual.loaded? 85 | refute actual.map { |c| c.emails.loaded? }.any? 86 | assert_equal expected, actual 87 | assert_equal expected.to_a, actual.to_a 88 | end 89 | 90 | def test_signed_global_id 91 | Campaign.forge! 5, emails: 3, subject: "Flash Sale 123" 92 | expected = Campaign.email_subjects_like("Sale") 93 | expected.load 94 | expected.map { |c| c.emails.load } 95 | 96 | assert_equal 5, Campaign.count 97 | assert_equal 15, Email.count 98 | assert expected.loaded? 99 | assert expected.map { |c| c.emails.loaded? }.any? 100 | 101 | sgid = URI::UID.build(expected).to_sgid_param 102 | uid = URI::UID.from_sgid(sgid) 103 | actual = uid.decode 104 | 105 | refute actual.loaded? 106 | refute actual.map { |c| c.emails.loaded? }.any? 107 | assert_equal expected, actual 108 | assert_equal expected.to_a, actual.to_a 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /test/universalid/extensions/active_record/active_record_unpersisted_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::ActiveRecordUnpersistedTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = Campaign.forge 6 | packed = UniversalID::Packer.pack(expected, include_changes: true) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal expected.attributes, actual.attributes 10 | end 11 | end 12 | 13 | class UniversalID::Encoder::ActiveRecordUnpersistedTest < Minitest::Test 14 | def test_encode_decode 15 | expected = Campaign.forge 16 | encoded = UniversalID::Encoder.encode(expected, include_changes: true) 17 | actual = UniversalID::Encoder.decode(encoded) 18 | 19 | assert_equal expected.attributes, actual.attributes 20 | end 21 | end 22 | 23 | class URI::UID::ActiveRecordUnpersistedTest < Minitest::Test 24 | def test_build_parse_decode 25 | expected = Campaign.forge 26 | uri = URI::UID.build(expected, include_changes: true).to_s 27 | uid = URI::UID.parse(uri) 28 | actual = uid.decode 29 | 30 | assert_equal expected.attributes, actual.attributes 31 | end 32 | 33 | def test_global_id 34 | expected = Campaign.forge 35 | gid = URI::UID.build(expected, include_changes: true).to_gid_param 36 | uid = URI::UID.from_gid(gid) 37 | actual = uid.decode 38 | 39 | assert_equal expected.attributes, actual.attributes 40 | end 41 | 42 | def test_signed_global_id 43 | expected = Campaign.forge 44 | sgid = URI::UID.build(expected, include_changes: true).to_sgid_param 45 | uid = URI::UID.from_sgid(sgid) 46 | actual = uid.decode 47 | 48 | assert_equal expected.attributes, actual.attributes 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/universalid/extensions/active_support/cache/store_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::ActiveSupportCacheStoreTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = ActiveSupport::Cache::MemoryStore.new 6 | scalars.each { |key, val| expected.write key, val, expires_in: 1.second } 7 | packed = UniversalID::Packer.pack(expected) 8 | actual = UniversalID::Packer.unpack(packed) 9 | assert_active_support_cache_store expected, actual, expires_in: 1.second 10 | end 11 | end 12 | 13 | class UniversalID::Encoder::ActiveSupportCacheStoreTest < Minitest::Test 14 | def test_encode_decode 15 | expected = ActiveSupport::Cache::MemoryStore.new 16 | scalars.each { |key, val| expected.write key, val, expires_in: 1.second } 17 | encoded = UniversalID::Encoder.encode(expected) 18 | actual = UniversalID::Encoder.decode(encoded) 19 | assert_active_support_cache_store expected, actual, expires_in: 1.second 20 | end 21 | end 22 | 23 | class URI::UID::ActiveSupportCacheStoreTest < Minitest::Test 24 | def test_build_parse_decode 25 | expected = ActiveSupport::Cache::MemoryStore.new 26 | scalars.each { |key, val| expected.write key, val, expires_in: 1.second } 27 | uri = URI::UID.build(expected).to_s 28 | actual = URI::UID.parse(uri).decode 29 | assert_active_support_cache_store expected, actual, expires_in: 1.second 30 | end 31 | 32 | def test_global_id 33 | expected = ActiveSupport::Cache::MemoryStore.new 34 | scalars.each { |key, val| expected.write key, val, expires_in: 1.second } 35 | gid = URI::UID.build(expected).to_gid_param 36 | actual = URI::UID.from_gid(gid).decode 37 | assert_active_support_cache_store expected, actual, expires_in: 1.second 38 | end 39 | 40 | def test_signed_global_id 41 | expected = ActiveSupport::Cache::MemoryStore.new 42 | scalars.each { |key, val| expected.write key, val, expires_in: 1.second } 43 | sgid = URI::UID.build(expected).to_sgid_param 44 | actual = URI::UID.from_sgid(sgid).decode 45 | assert_active_support_cache_store expected, actual, expires_in: 1.second 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/universalid/extensions/active_support/time_with_zone_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::TimeWithZoneTest < Minitest::Test 4 | def test_pack_unpack 5 | time_with_zones do |expected| 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_kind_of ActiveSupport::TimeWithZone, expected 10 | assert_kind_of ActiveSupport::TimeWithZone, actual 11 | assert_equal expected, actual 12 | end 13 | end 14 | end 15 | 16 | class UniversalID::Encoder::TimeWithZoneTest < Minitest::Test 17 | def test_encode_decode 18 | time_with_zones do |expected| 19 | encoded = UniversalID::Encoder.encode(expected) 20 | actual = UniversalID::Encoder.decode(encoded) 21 | 22 | assert_kind_of ActiveSupport::TimeWithZone, expected 23 | assert_kind_of ActiveSupport::TimeWithZone, actual 24 | assert_equal expected, actual 25 | end 26 | end 27 | end 28 | 29 | class URI::UID::TimeWithZoneTest < Minitest::Test 30 | def test_build_parse_decode 31 | time_with_zones do |expected| 32 | uri = URI::UID.build(expected).to_s 33 | uid = URI::UID.parse(uri) 34 | actual = uid.decode 35 | 36 | assert_kind_of ActiveSupport::TimeWithZone, expected 37 | assert_kind_of ActiveSupport::TimeWithZone, actual 38 | assert_equal expected, actual 39 | end 40 | end 41 | 42 | def test_global_id 43 | time_with_zones do |expected| 44 | gid = URI::UID.build(expected).to_gid_param 45 | uid = URI::UID.from_gid(gid) 46 | actual = uid.decode 47 | 48 | assert_kind_of ActiveSupport::TimeWithZone, expected 49 | assert_kind_of ActiveSupport::TimeWithZone, actual 50 | assert_equal expected, actual 51 | end 52 | end 53 | 54 | def test_signed_global_id 55 | time_with_zones do |expected| 56 | sgid = URI::UID.build(expected).to_sgid_param 57 | uid = URI::UID.from_sgid(sgid) 58 | actual = uid.decode 59 | 60 | assert_kind_of ActiveSupport::TimeWithZone, expected 61 | assert_kind_of ActiveSupport::TimeWithZone, actual 62 | assert_equal expected, actual 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/universalid/extensions/global_id/global_id_model_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::GlobalIDModelTest < Minitest::Test 4 | def test_new_from_uid 5 | uid = URI::UID.build(scalars) 6 | model = UniversalID::Extensions::GlobalIDModel.new(uid) 7 | 8 | assert_instance_of UniversalID::Extensions::GlobalIDModel, model 9 | assert_equal uid, model.uid 10 | assert_equal scalars, model.uid.decode 11 | end 12 | 13 | def test_new_from_uid_payload 14 | uid = URI::UID.build(scalars) 15 | model = UniversalID::Extensions::GlobalIDModel.new(uid.payload) 16 | 17 | assert_instance_of UniversalID::Extensions::GlobalIDModel, model 18 | assert_equal uid, model.uid 19 | assert_equal scalars, model.uid.decode 20 | end 21 | 22 | def test_new_from_uri 23 | uri = URI::UID.build(scalars).to_s 24 | model = UniversalID::Extensions::GlobalIDModel.new(uri) 25 | 26 | assert_instance_of UniversalID::Extensions::GlobalIDModel, model 27 | assert_equal uri, model.uid.to_s 28 | assert_equal scalars, model.uid.decode 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/universalid/extensions/global_id/global_id_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::GlobalIDTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = Campaign.forge!.to_gid 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal expected, actual 10 | end 11 | end 12 | 13 | class UniversalID::Encoder::GlobalIDTest < Minitest::Test 14 | def test_encode_decode 15 | expected = Campaign.forge!.to_gid 16 | encoded = UniversalID::Encoder.encode(expected) 17 | actual = UniversalID::Encoder.decode(encoded) 18 | 19 | assert_equal expected, actual 20 | end 21 | end 22 | 23 | class URI::UID::GlobalIDTest < Minitest::Test 24 | def test_build_parse_decode 25 | expected = Campaign.forge!.to_gid 26 | uri = URI::UID.build(expected).to_s 27 | uid = URI::UID.parse(uri) 28 | actual = uid.decode 29 | 30 | assert_equal expected, actual 31 | end 32 | 33 | def test_global_id 34 | expected = Campaign.forge!.to_gid 35 | gid = URI::UID.build(expected).to_gid_param 36 | uid = URI::UID.from_gid(gid) 37 | actual = uid.decode 38 | 39 | assert_equal expected, actual 40 | end 41 | 42 | def test_signed_global_id 43 | expected = Campaign.forge!.to_gid 44 | sgid = URI::UID.build(expected).to_sgid_param 45 | uid = URI::UID.from_sgid(sgid) 46 | actual = uid.decode 47 | 48 | assert_equal expected, actual 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/universalid/extensions/signed_global_id/signed_global_id_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::SignedGlobalIDTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = Campaign.forge!.to_sgid 6 | packed = UniversalID::Packer.pack(expected) 7 | unpacked = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal expected, unpacked 10 | end 11 | end 12 | 13 | class UniversalID::Encoder::SignedGlobalIDTest < Minitest::Test 14 | def test_encode_decode 15 | expected = Campaign.forge!.to_sgid 16 | encoded = UniversalID::Encoder.encode(expected) 17 | decoded = UniversalID::Encoder.decode(encoded) 18 | 19 | assert_equal expected, decoded 20 | end 21 | end 22 | 23 | class URI::UID::SignedGlobalIDTest < Minitest::Test 24 | def test_build_parse_decode 25 | expected = Campaign.forge!.to_sgid 26 | uri = URI::UID.build(expected).to_s 27 | uid = URI::UID.parse(uri) 28 | decoded = uid.decode 29 | 30 | assert_equal expected, decoded 31 | end 32 | 33 | def test_global_id 34 | expected = Campaign.forge!.to_sgid 35 | gid = URI::UID.build(expected).to_gid_param 36 | uid = URI::UID.from_gid(gid) 37 | decoded = uid.decode 38 | 39 | assert_equal expected, decoded 40 | end 41 | 42 | def test_signed_global_id 43 | expected = Campaign.forge!.to_sgid 44 | sgid = URI::UID.build(expected).to_sgid_param 45 | uid = URI::UID.from_sgid(sgid) 46 | decoded = uid.decode 47 | 48 | assert_equal expected, decoded 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/composites/array_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::ArrayTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = scalars.values 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal expected, actual 10 | end 11 | end 12 | 13 | class UniversalID::Encoder::ArrayTest < Minitest::Test 14 | def test_encode_decode 15 | expected = scalars.values 16 | encoded = UniversalID::Encoder.encode(expected) 17 | actual = UniversalID::Encoder.decode(encoded) 18 | 19 | assert_equal expected, actual 20 | end 21 | end 22 | 23 | class URI::UID::ArrayTest < Minitest::Test 24 | def test_build_parse_decode 25 | expected = scalars.values 26 | uri = URI::UID.build(expected).to_s 27 | uid = URI::UID.parse(uri) 28 | actual = uid.decode 29 | 30 | assert_equal expected, actual 31 | end 32 | 33 | def test_global_id 34 | expected = scalars.values 35 | gid = URI::UID.build(expected).to_gid_param 36 | uid = URI::UID.from_gid(gid) 37 | actual = uid.decode 38 | 39 | assert_equal expected, actual 40 | end 41 | 42 | def test_signed_global_id 43 | expected = scalars.values 44 | sgid = URI::UID.build(expected).to_sgid_param 45 | uid = URI::UID.from_sgid(sgid) 46 | actual = uid.decode 47 | 48 | assert_equal expected, actual 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/composites/hash_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::HashTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = scalars 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal expected, actual 10 | end 11 | end 12 | 13 | class UniversalID::Encoder::HashTest < Minitest::Test 14 | def test_encode_decode 15 | expected = scalars 16 | encoded = UniversalID::Encoder.encode(expected) 17 | actual = UniversalID::Encoder.decode(encoded) 18 | 19 | assert_equal expected, actual 20 | end 21 | end 22 | 23 | class URI::UID::HashTest < Minitest::Test 24 | def test_build_parse_decode 25 | expected = scalars 26 | uri = URI::UID.build(expected).to_s 27 | uid = URI::UID.parse(uri) 28 | actual = uid.decode 29 | 30 | assert_equal expected, actual 31 | end 32 | 33 | def test_global_id 34 | expected = scalars 35 | gid = URI::UID.build(expected).to_gid_param 36 | uid = URI::UID.from_gid(gid) 37 | actual = uid.decode 38 | 39 | assert_equal expected, actual 40 | end 41 | 42 | def test_signed_global_id 43 | expected = scalars 44 | sgid = URI::UID.build(expected).to_sgid_param 45 | uid = URI::UID.from_sgid(sgid) 46 | actual = uid.decode 47 | 48 | assert_equal expected, actual 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/composites/open_struct.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::OpenStructTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = OpenStruct.new(scalars) 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal expected, actual 10 | end 11 | end 12 | 13 | class UniversalID::Encoder::OpenStructTest < Minitest::Test 14 | def test_encode_decode 15 | expected = OpenStruct.new(scalars) 16 | encoded = UniversalID::Encoder.encode(expected) 17 | actual = UniversalID::Encoder.decode(encoded) 18 | 19 | assert_equal expected, actual 20 | end 21 | end 22 | 23 | class URI::UID::OpenStructTest < Minitest::Test 24 | def test_build_parse_decode 25 | expected = OpenStruct.new(scalars) 26 | uri = URI::UID.build(expected).to_s 27 | uid = URI::UID.parse(uri) 28 | actual = uid.decode 29 | 30 | assert_equal expected, actual 31 | end 32 | 33 | def test_global_id 34 | expected = OpenStruct.new(scalars) 35 | gid = URI::UID.build(expected).to_gid_param 36 | uid = URI::UID.from_gid(gid) 37 | actual = uid.decode 38 | 39 | assert_equal expected, actual 40 | end 41 | 42 | def test_signed_global_id 43 | expected = OpenStruct.new(scalars) 44 | sgid = URI::UID.build(expected).to_sgid_param 45 | uid = URI::UID.from_sgid(sgid) 46 | actual = uid.decode 47 | 48 | assert_equal expected, actual 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/composites/set_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::SetTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = Set.new(scalars.values) 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal expected, actual 10 | end 11 | end 12 | 13 | class UniversalID::Encoder::SetTest < Minitest::Test 14 | def test_encode_decode 15 | expected = Set.new(scalars.values) 16 | encoded = UniversalID::Encoder.encode(expected) 17 | actual = UniversalID::Encoder.decode(encoded) 18 | 19 | assert_equal expected, actual 20 | end 21 | end 22 | 23 | class URI::UID::SetTest < Minitest::Test 24 | def test_build_parse_decode 25 | expected = Set.new(scalars.values) 26 | uri = URI::UID.build(expected).to_s 27 | uid = URI::UID.parse(uri) 28 | actual = uid.decode 29 | 30 | assert_equal expected, actual 31 | end 32 | 33 | def test_global_id 34 | expected = Set.new(scalars.values) 35 | gid = URI::UID.build(expected).to_gid_param 36 | uid = URI::UID.from_gid(gid) 37 | actual = uid.decode 38 | 39 | assert_equal expected, actual 40 | end 41 | 42 | def test_signed_global_id 43 | expected = Set.new(scalars.values) 44 | sgid = URI::UID.build(expected).to_sgid_param 45 | uid = URI::UID.from_sgid(sgid) 46 | actual = uid.decode 47 | 48 | assert_equal expected, actual 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/composites/struct_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::StructTest < Minitest::Test 4 | def test_pack_unpack_dynamic 5 | expected = Struct.new(*scalars.keys).new(*scalars.values) 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal expected.to_h, actual.to_h 10 | end 11 | 12 | class Scalars < Struct.new(*scalars.keys); end 13 | 14 | def test_pack_unpack_concrete 15 | expected = Scalars.new(*scalars.values) 16 | packed = UniversalID::Packer.pack(expected) 17 | actual = UniversalID::Packer.unpack(packed) 18 | 19 | assert_equal expected, actual 20 | end 21 | end 22 | 23 | class UniversalID::Encoder::StructTest < Minitest::Test 24 | def test_encode_decode_dynamic 25 | expected = Struct.new(*scalars.keys).new(*scalars.values) 26 | encoded = UniversalID::Encoder.encode(expected) 27 | actual = UniversalID::Encoder.decode(encoded) 28 | 29 | assert_equal expected.to_h, actual.to_h 30 | end 31 | 32 | class Scalars < Struct.new(*scalars.keys); end 33 | 34 | def test_encode_decode_concrete 35 | expected = Scalars.new(*scalars.values) 36 | encoded = UniversalID::Encoder.encode(expected) 37 | actual = UniversalID::Encoder.decode(encoded) 38 | 39 | assert_equal expected, actual 40 | end 41 | end 42 | 43 | class URI::UID::StructTest < Minitest::Test 44 | def test_build_parse_decode_dynamic 45 | expected = Struct.new(*scalars.keys).new(*scalars.values) 46 | uri = URI::UID.build(expected).to_s 47 | uid = URI::UID.parse(uri) 48 | actual = uid.decode 49 | 50 | assert_equal expected.to_h, actual.to_h 51 | end 52 | 53 | def test_global_id_dynamic 54 | expected = Struct.new(*scalars.keys).new(*scalars.values) 55 | gid = URI::UID.build(expected).to_gid_param 56 | uid = URI::UID.from_gid(gid) 57 | actual = uid.decode 58 | 59 | assert_equal expected.to_h, actual.to_h 60 | end 61 | 62 | def test_signed_global_id_dynamic 63 | expected = Struct.new(*scalars.keys).new(*scalars.values) 64 | sgid = URI::UID.build(expected).to_sgid_param 65 | uid = URI::UID.from_sgid(sgid) 66 | actual = uid.decode 67 | 68 | assert_equal expected.to_h, actual.to_h 69 | end 70 | 71 | class Scalars < Struct.new(*scalars.keys); end 72 | 73 | def test_build_parse_decode_concrete 74 | expected = Scalars.new(*scalars.values) 75 | uri = URI::UID.build(expected).to_s 76 | uid = URI::UID.parse(uri) 77 | actual = uid.decode 78 | 79 | assert_equal expected, actual 80 | end 81 | 82 | def test_global_id_concrete 83 | expected = Scalars.new(*scalars.values) 84 | gid = URI::UID.build(expected).to_gid_param 85 | uid = URI::UID.from_gid(gid) 86 | actual = uid.decode 87 | 88 | assert_equal expected, actual 89 | end 90 | 91 | def test_signed_global_id_concrete 92 | expected = Scalars.new(*scalars.values) 93 | sgid = URI::UID.build(expected).to_sgid_param 94 | uid = URI::UID.from_sgid(sgid) 95 | actual = uid.decode 96 | 97 | assert_equal expected, actual 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/big_decimal_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::BigDecimalTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = BigDecimal("9876543210.0123456789") 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 25, packed.size 10 | assert_equal "\xC7\x16\x01\xB59876543210.0123456789".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::BigDecimalTest < Minitest::Test 16 | def test_encode_decode 17 | expected = BigDecimal("9876543210.0123456789") 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 39, encoded.size 22 | assert_equal "CwyAxxYBtTk4NzY1NDMyMTAuMDEyMzQ1Njc4OQM", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::BigDecimalTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = BigDecimal("9876543210.0123456789") 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/CwyAxxYBtTk4NzY1NDMyMTAuMDEyMzQ1Njc4OQM") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = BigDecimal("9876543210.0123456789") 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = BigDecimal("9876543210.0123456789") 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/complex_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::ComplexTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = Complex(2, 3) 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 8, packed.size 10 | assert_equal "\xC7\x05\x02\xA42+3i".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::ComplexTest < Minitest::Test 16 | def test_encode_decode 17 | expected = Complex(2, 3) 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 16, encoded.size 22 | assert_equal "iwOAxwUCpDIrM2kD", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::ComplexTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = Complex(2, 3) 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/iwOAxwUCpDIrM2kD") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = Complex(2, 3) 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = Complex(2, 3) 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/date_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::DateTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = Date.parse("2024-01-28") 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 14, packed.size 10 | assert_equal "\xC7\v\x05\xAA2024-01-28".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::DateTest < Minitest::Test 16 | def test_encode_decode 17 | expected = Date.parse("2024-01-28") 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 24, encoded.size 22 | assert_equal "iwaAxwsFqjIwMjQtMDEtMjgD", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::DateTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = Date.parse("2024-01-28") 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/iwaAxwsFqjIwMjQtMDEtMjgD") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = Date.parse("2024-01-28") 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = Date.parse("2024-01-28") 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/date_time_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::DateTimeTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = DateTime.new(2023, 2, 3, 4, 5, 6) 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 40, packed.size 10 | assert_equal "\xC7%\x04\xD9#2023-02-03T04:05:06.000000000+00:00".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::DateTimeTest < Minitest::Test 16 | def test_encode_decode 17 | expected = DateTime.new(2023, 2, 3, 4, 5, 6) 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 56, encoded.size 22 | assert_equal "GycA-I2UqT1eHWcT5K1IzSR7uz2EL1lpAlIAMIy2p9whw_5OueUtAFMB", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::DateTimeTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = DateTime.new(2023, 2, 3, 4, 5, 6) 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/GycA-I2UqT1eHWcT5K1IzSR7uz2EL1lpAlIAMIy2p9whw_5OueUtAFMB") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = DateTime.new(2023, 2, 3, 4, 5, 6) 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = DateTime.new(2023, 2, 3, 4, 5, 6) 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/false_class_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::FalseClassTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = false 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 1, packed.size 10 | assert_equal "\xC2".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::FalseClassTest < Minitest::Test 16 | def test_encode_decode 17 | expected = false 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 7, encoded.size 22 | assert_equal "CwCAwgM", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::FalseClassTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = false 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/CwCAwgM") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = false 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = false 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/float_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::FloatTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = 789.456 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 9, packed.size 10 | assert_equal "\xCB@\x88\xAB\xA5\xE3S\xF7\xCF".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::FloatTest < Minitest::Test 16 | def test_encode_decode 17 | expected = 789.456 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 18, encoded.size 22 | assert_equal "CwSAy0CIq6XjU_fPAw", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::FloatTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = 789.456 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/CwSAy0CIq6XjU_fPAw") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = 789.456 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = 789.456 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/integer_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::IntegerTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = 758423 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 5, packed.size 10 | assert_equal "\xCE\x00\v\x92\x97".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::IntegerTest < Minitest::Test 16 | def test_encode_decode 17 | expected = 758423 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 12, encoded.size 22 | assert_equal "CwKAzgALkpcD", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::IntegerTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = 758423 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/CwKAzgALkpcD") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = 758423 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = 758423 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/nil_class_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::NilClassTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = nil 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 1, packed.size 10 | assert_equal "\xC0".b, packed 11 | assert_nil actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::NilClassTest < Minitest::Test 16 | def test_encode_decode 17 | expected = nil 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 7, encoded.size 22 | assert_equal "CwCAwAM", encoded 23 | assert_nil actual 24 | end 25 | end 26 | 27 | class URI::UID::NilClassTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = nil 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/CwCAwAM") 35 | assert_nil actual 36 | end 37 | 38 | def test_global_id 39 | expected = nil 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_nil actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = nil 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_nil actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/range_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::RangeTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = (7..42) 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 8, packed.size 10 | assert_equal "\xC7\x05\x06\a\xA2..*".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::RangeTest < Minitest::Test 16 | def test_encode_decode 17 | expected = (7..42) 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 16, encoded.size 22 | assert_equal "iwOAxwUGB6IuLioD", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::RangeTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = (7..42) 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/iwOAxwUGB6IuLioD") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = (7..42) 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = (7..42) 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/rational_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::RationalTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = Rational(24, 3) 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 6, packed.size 10 | assert_equal "\xD6\x03\xA38/1".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::RationalTest < Minitest::Test 16 | def test_encode_decode 17 | expected = Rational(24, 3) 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 14, encoded.size 22 | assert_equal "iwKA1gOjOC8xAw", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::RationalTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = Rational(24, 3) 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/iwKA1gOjOC8xAw") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = Rational(24, 3) 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = Rational(24, 3) 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/regexp_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::RegexpTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = /\Aexample\d{2,}/i 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 20, packed.size 10 | assert_equal "\xC7\x11\a\xAF\\Aexample\\d{2,}\x01".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::RegexpTest < Minitest::Test 16 | def test_encode_decode 17 | expected = /\Aexample\d{2,}/i 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 32, encoded.size 22 | assert_equal "iwmAxxEHr1xBZXhhbXBsZVxkezIsfQED", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::RegexpTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = /\Aexample\d{2,}/i 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/iwmAxxEHr1xBZXhhbXBsZVxkezIsfQED") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = /\Aexample\d{2,}/i 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = /\Aexample\d{2,}/i 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/string_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::StringTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = "This is a string!" 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 18, packed.size 10 | assert_equal "\xB1This is a string!".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::StringTest < Minitest::Test 16 | def test_encode_decode 17 | expected = "This is a string!" 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 27, encoded.size 22 | assert_equal "GxEA-KVBQsJipSKNUFKwLNSnHEI", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::StringTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = "This is a string!" 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/GxEA-KVBQsJipSKNUFKwLNSnHEI") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = "This is a string!" 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = "This is a string!" 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/symbol_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::SymbolTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = :test_symbol 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 14, packed.size 10 | assert_equal "\xC7\v\x00test_symbol".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::SymbolTest < Minitest::Test 16 | def test_encode_decode 17 | expected = :test_symbol 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 24, encoded.size 22 | assert_equal "iwaAxwsAdGVzdF9zeW1ib2wD", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::SymbolTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = :test_symbol 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/iwaAxwsAdGVzdF9zeW1ib2wD") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = :test_symbol 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = :test_symbol 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/time_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::TimeTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = Time.new(2024, 4, 17, 8, 22, 39, 42) 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 6, packed.size 10 | assert_equal "\xD6\xFFf\x1F\x86\xA5".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::TimeTest < Minitest::Test 16 | def test_encode_decode 17 | expected = Time.new(2024, 4, 17, 8, 22, 39, 42) 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 14, encoded.size 22 | assert_equal "iwKA1v9mH4alAw", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::TimeTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = Time.new(2024, 4, 17, 8, 22, 39, 42) 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/iwKA1v9mH4alAw") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = Time.new(2024, 4, 17, 8, 22, 39, 42) 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = Time.new(2024, 4, 17, 8, 22, 39, 42) 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/message_pack_types/scalars/true_class_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Packer::TrueClassTest < Minitest::Test 4 | def test_pack_unpack 5 | expected = true 6 | packed = UniversalID::Packer.pack(expected) 7 | actual = UniversalID::Packer.unpack(packed) 8 | 9 | assert_equal 1, packed.size 10 | assert_equal "\xC3".b, packed 11 | assert_equal expected, actual 12 | end 13 | end 14 | 15 | class UniversalID::Encoder::TrueClassTest < Minitest::Test 16 | def test_encode_decode 17 | expected = true 18 | encoded = UniversalID::Encoder.encode(expected) 19 | actual = UniversalID::Encoder.decode(encoded) 20 | 21 | assert_equal 7, encoded.size 22 | assert_equal "CwCAwwM", encoded 23 | assert_equal expected, actual 24 | end 25 | end 26 | 27 | class URI::UID::TrueClassTest < Minitest::Test 28 | def test_build_parse_decode 29 | expected = true 30 | uri = URI::UID.build(expected).to_s 31 | uid = URI::UID.parse(uri) 32 | actual = uid.decode 33 | 34 | assert uri.start_with?("uid://universalid/CwCAwwM") 35 | assert_equal expected, actual 36 | end 37 | 38 | def test_global_id 39 | expected = true 40 | gid = URI::UID.build(expected).to_gid_param 41 | uid = URI::UID.from_gid(gid) 42 | actual = uid.decode 43 | 44 | assert_equal expected, actual 45 | end 46 | 47 | def test_signed_global_id 48 | expected = true 49 | sgid = URI::UID.build(expected).to_sgid_param 50 | uid = URI::UID.from_sgid(sgid) 51 | actual = uid.decode 52 | 53 | assert_equal expected, actual 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/universalid/prepack_options_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::PrepackOptionsTest < Minitest::Test 4 | def test_new 5 | options = UniversalID::PrepackOptions.new 6 | expected = { 7 | exclude: [], 8 | include: [], 9 | include_blank: true, 10 | database: { 11 | include_keys: true, 12 | include_timestamps: true, 13 | include_changes: false, 14 | include_descendants: false, 15 | descendant_depth: 0 16 | } 17 | } 18 | assert_equal expected, options.to_h 19 | assert_equal UniversalID::Settings.default.prepack.to_h, options.to_h 20 | end 21 | 22 | def test_new_with_override 23 | options = UniversalID::PrepackOptions.new(exclude: [:created_at, :updated_at], include_blank: false) 24 | expected = { 25 | exclude: [ 26 | :created_at, 27 | :updated_at 28 | ], 29 | include: [], 30 | include_blank: false, 31 | database: { 32 | include_keys: true, 33 | include_timestamps: true, 34 | include_changes: false, 35 | include_descendants: false, 36 | descendant_depth: 0 37 | } 38 | } 39 | assert_equal expected, options.to_h 40 | assert options.reject_key?(:created_at) 41 | assert options.reject_key?("created_at") 42 | assert options.reject_value?(nil) 43 | assert options.reject_value?([]) 44 | assert options.reject_value?({}) 45 | end 46 | 47 | def test_new_database_options 48 | options = UniversalID::PrepackOptions.new 49 | expected = { 50 | include_keys: true, 51 | include_timestamps: true, 52 | include_changes: false, 53 | include_descendants: false, 54 | descendant_depth: 0 55 | } 56 | assert_equal expected, options.database_options.to_h 57 | end 58 | 59 | def test_new_database_options_with_override 60 | options = UniversalID::PrepackOptions.new(include_keys: false, include_timestamps: false) 61 | expected = { 62 | include_keys: false, 63 | include_timestamps: false, 64 | include_changes: false, 65 | include_descendants: false, 66 | descendant_depth: 0 67 | } 68 | assert_equal expected, options.database_options.to_h 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/universalid/prepacker/array_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Prepacker::ArrayTest < Minitest::Test 4 | def setup 5 | @array = [Date.today, nil, "", " ", "\t", "\r", "\n", "\r\n", "string", true, false, 123, [], [nil, "", "string", true, false, 123], {}, {a: 1}] 6 | @deep_array = [ 7 | Date.today, nil, "", " ", "\t", "\r", "\n", "\r\n", "string", true, false, 123, [], [nil, "", "string", true, false, 123], {}, {a: 1}, 8 | [Date.today, nil, "", " ", "\t", "\r", "\n", "\r\n", "string", true, false, 123, [], [nil, "", "string", true, false, 123], {}, {a: 1}, 9 | [Date.today, nil, "", " ", "\t", "\r", "\n", "\r\n", "string", true, false, 123, [], [nil, "", "string", true, false, 123], {}, {a: 1}, 10 | [Date.today, nil, "", " ", "\t", "\r", "\n", "\r\n", "string", true, false, 123, [], [nil, "", "string", true, false, 123], {}, {a: 1}]]] 11 | ] 12 | end 13 | 14 | def test_array_without_override 15 | prepacked = UniversalID::Prepacker.prepack(@array) 16 | assert_equal @array, prepacked 17 | end 18 | 19 | def test_array_with_override 20 | prepacked = UniversalID::Prepacker.prepack(@array, include_blank: false) 21 | expected = [Date.today, "string", true, false, 123, ["string", true, false, 123], {a: 1}] 22 | assert_equal expected, prepacked 23 | end 24 | 25 | def test_deep_array_without_override 26 | prepacked = UniversalID::Prepacker.prepack(@deep_array) 27 | assert_equal @deep_array, prepacked 28 | end 29 | 30 | def test_deep_array_with_override 31 | prepacked = UniversalID::Prepacker.prepack(@deep_array, include_blank: false) 32 | expected = [ 33 | Date.today, "string", true, false, 123, ["string", true, false, 123], {a: 1}, 34 | [Date.today, "string", true, false, 123, ["string", true, false, 123], {a: 1}, 35 | [Date.today, "string", true, false, 123, ["string", true, false, 123], {a: 1}, 36 | [Date.today, "string", true, false, 123, ["string", true, false, 123], {a: 1}]]] 37 | ] 38 | assert_equal expected, prepacked 39 | end 40 | 41 | def test_self_references 42 | array = Marshal.load(Marshal.dump(@array)) 43 | array << array.dup 44 | array.last << array.dup 45 | 46 | assert_raises(UniversalID::Prepacker::CircularReferenceError) do 47 | UniversalID::Prepacker.prepack array 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/universalid/prepacker/hash_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::Prepacker::HashTest < Minitest::Test 4 | def setup 5 | @hash = { 6 | one: Date.today, 7 | two: nil, 8 | three: "", 9 | four: " ", 10 | five: "\t", 11 | six: "\r", 12 | seven: "\n", 13 | eight: "\r\n", 14 | nine: "string", 15 | ten: true, 16 | eleven: false, 17 | twelve: 123, 18 | thirteen: [], 19 | fourteen: [nil, "", "string", true, false, 123], 20 | fifteen: {}, 21 | sixteen: {a: 1} 22 | } 23 | @deep_hash = Marshal.load(Marshal.dump(@hash)) 24 | @deep_hash[:seventeen] = Marshal.load(Marshal.dump(@deep_hash.dup)) 25 | @deep_hash[:seventeen][:eighteen] = Marshal.load(Marshal.dump(@deep_hash.dup)) 26 | end 27 | 28 | def test_hash_without_override 29 | prepacked = UniversalID::Prepacker.prepack(@hash) 30 | assert_equal @hash, prepacked 31 | end 32 | 33 | def test_hash_with_override 34 | prepacked = UniversalID::Prepacker.prepack(@hash, include_blank: false) 35 | expected = { 36 | one: Date.today, 37 | nine: "string", 38 | ten: true, 39 | eleven: false, 40 | twelve: 123, 41 | fourteen: ["string", true, false, 123], 42 | sixteen: {a: 1} 43 | } 44 | assert_equal expected, prepacked 45 | end 46 | 47 | def test_deep_hash_without_override 48 | prepacked = UniversalID::Prepacker.prepack(@deep_hash) 49 | assert_equal @deep_hash, prepacked 50 | end 51 | 52 | def test_deep_hash_with_override 53 | prepacked = UniversalID::Prepacker.prepack(@deep_hash, include_blank: false) 54 | expected = { 55 | one: Date.today, 56 | nine: "string", 57 | ten: true, 58 | eleven: false, 59 | twelve: 123, 60 | fourteen: ["string", true, false, 123], 61 | sixteen: {a: 1}, 62 | seventeen: { 63 | one: Date.today, 64 | nine: "string", 65 | ten: true, 66 | eleven: false, 67 | twelve: 123, 68 | fourteen: ["string", true, false, 123], 69 | sixteen: {a: 1}, 70 | eighteen: { 71 | one: Date.today, 72 | nine: "string", 73 | ten: true, 74 | eleven: false, 75 | twelve: 123, 76 | fourteen: ["string", true, false, 123], 77 | sixteen: {a: 1}, 78 | seventeen: { 79 | one: Date.today, 80 | nine: "string", 81 | ten: true, 82 | eleven: false, 83 | twelve: 123, 84 | fourteen: ["string", true, false, 123], 85 | sixteen: {a: 1} 86 | } 87 | } 88 | } 89 | } 90 | assert_equal expected, prepacked 91 | end 92 | 93 | def test_self_references 94 | hash = Marshal.load(Marshal.dump(@hash)) 95 | hash[:seventeen] = hash.dup 96 | hash[:seventeen][:eighteen] = hash.dup 97 | 98 | assert_raises(UniversalID::Prepacker::CircularReferenceError) do 99 | UniversalID::Prepacker.prepack hash 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /test/universalid/readme_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::ReadmeTest < Minitest::Test 4 | def test_unsaved_changes_on_new_records 5 | campaign = new_campaign 6 | 7 | assert_new_record campaign 8 | 9 | options = { 10 | include_changes: true, 11 | include_descendants: true, 12 | descendant_depth: 2 13 | } 14 | 15 | encoded = URI::UID.build(campaign, options).to_s 16 | decoded = URI::UID.parse(encoded).decode 17 | 18 | assert_new_record decoded 19 | end 20 | 21 | def test_unsaved_changes_on_persisted_records 22 | campaign = new_campaign 23 | campaign.save! 24 | 25 | assert_persisted_record campaign 26 | 27 | campaign.name = "Campaign #{SecureRandom.hex}" 28 | campaign.emails.each do |email| 29 | email.subject = "Email #{SecureRandom.hex}" 30 | email.attachments.each do |attachment| 31 | attachment.file_name = "Attachment-#{SecureRandom.hex}.pdf" 32 | end 33 | end 34 | 35 | options = { 36 | include_changes: true, 37 | include_descendants: true, 38 | descendant_depth: 2 39 | } 40 | 41 | encoded = URI::UID.build(campaign, options).to_s 42 | decoded = URI::UID.parse(encoded).decode 43 | 44 | assert_persisted_record decoded, changes_expected: true 45 | end 46 | 47 | def test_copy_persisted_records 48 | campaign = new_campaign 49 | campaign.save! 50 | 51 | assert_persisted_record campaign 52 | 53 | options = { 54 | include_keys: false, 55 | include_timestamps: false, 56 | include_changes: true, 57 | include_descendants: true, 58 | descendant_depth: 2 59 | } 60 | 61 | encoded = URI::UID.build(campaign, options).to_s 62 | decoded = URI::UID.parse(encoded).decode 63 | 64 | assert_new_record decoded 65 | decoded.save! 66 | assert_persisted_record decoded 67 | 68 | refute_equal campaign.id, decoded.id 69 | 70 | campaign.emails.each do |email| 71 | assert (campaign.emails.map(&:id) & decoded.emails.map(&:id)).none? 72 | assert (campaign.emails.map(&:attachments).flatten.map(&:id) & decoded.emails.map(&:attachments).flatten.map(&:id)).none? 73 | end 74 | end 75 | 76 | private 77 | 78 | def new_campaign 79 | campaign = Campaign.new( 80 | name: "Summer Sale Campaign", 81 | description: "A campaign for the summer sale, targeting our loyal customers.", 82 | trigger: "SummerStart" 83 | ) 84 | 85 | campaign.emails = 3.times.map do |i| 86 | email = campaign.emails.build( 87 | subject: "Summer Sale Special Offer #{i + 1}", 88 | body: "Dear Customer, check out our exclusive summer sale offers! #{i + 1}", 89 | wait: rand(1..14) 90 | ) 91 | 92 | email.tap do |e| 93 | e.attachments = 2.times.map do |j| 94 | data = SecureRandom.random_bytes(rand(500..1500)) 95 | e.attachments.build( 96 | file_name: "summer_sale_#{i + 1}_attachment_#{j + 1}.pdf", 97 | content_type: "application/pdf", 98 | file_size: data.size, 99 | file_data: data 100 | ) 101 | end 102 | end 103 | end 104 | 105 | campaign 106 | end 107 | 108 | def assert_new_record(campaign) 109 | assert campaign.new_record? 110 | assert campaign.changed? 111 | assert_equal 3, campaign.emails.size 112 | 113 | campaign.emails.each do |email| 114 | assert email.new_record? 115 | assert email.changed? 116 | assert_equal 2, email.attachments.size 117 | 118 | email.attachments.each do |attachment| 119 | assert attachment.new_record? 120 | assert attachment.changed? 121 | end 122 | end 123 | end 124 | 125 | def assert_persisted_record(campaign, changes_expected: false) 126 | assert campaign.persisted? 127 | assert campaign.changed? if changes_expected 128 | assert_equal 3, campaign.emails.size 129 | assert campaign.emails.loaded? 130 | 131 | campaign.emails.each do |email| 132 | assert email.persisted? 133 | assert email.changed? if changes_expected 134 | assert_equal 2, email.attachments.size 135 | assert email.attachments.loaded? 136 | 137 | email.attachments.each do |attachment| 138 | assert attachment.persisted? 139 | assert attachment.changed? if changes_expected 140 | end 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /test/universalid/settings_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::SettingsTest < Minitest::Test 4 | def test_default_settings 5 | expected = { 6 | prepack: { 7 | exclude: [], 8 | include: [], 9 | include_blank: true, 10 | 11 | database: { 12 | include_keys: true, 13 | include_timestamps: true, 14 | include_changes: false, 15 | include_descendants: false, 16 | descendant_depth: 0 17 | } 18 | } 19 | } 20 | assert_equal expected, UniversalID::Settings.default.to_h 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/universalid/universal_id_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UniversalID::UniversalIDTest < Minitest::Test 4 | def test_uid_to_gid_back_to_uid 5 | campaign = Campaign.forge! emails: 3, attachments: 2 6 | 7 | uid = URI::UID.build(campaign) 8 | gid_param = uid.to_gid_param 9 | 10 | parsed_gid = GlobalID.parse(gid_param) 11 | parsed_uid = parsed_gid.find.uid 12 | 13 | parsed_campaign = parsed_uid.decode 14 | 15 | assert_equal campaign, parsed_campaign 16 | assert_equal campaign.emails, parsed_campaign.emails 17 | assert_equal campaign.emails.map(&:attachments), parsed_campaign.emails.map(&:attachments) 18 | assert parsed_campaign.emails.loaded? 19 | assert parsed_campaign.emails.map(&:attachments).map(&:loaded) 20 | end 21 | 22 | def test_uid_to_sgid_back_to_uid 23 | campaign = Campaign.forge! emails: 3, attachments: 2 24 | 25 | uid = URI::UID.build(campaign) 26 | gid_param = uid.to_sgid_param 27 | 28 | parsed_gid = SignedGlobalID.parse(gid_param) 29 | parsed_uid = parsed_gid.find.uid 30 | 31 | parsed_campaign = parsed_uid.decode 32 | 33 | assert_equal campaign, parsed_campaign 34 | assert_equal campaign.emails, parsed_campaign.emails 35 | assert_equal campaign.emails.map(&:attachments), parsed_campaign.emails.map(&:attachments) 36 | assert parsed_campaign.emails.loaded? 37 | assert parsed_campaign.emails.map(&:attachments).map(&:loaded) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/uri/uid/real_world_example_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class URI::UID::RealWorldExampleTest < Minitest::Test 4 | def test_complex_data_structure 5 | # create some active record object 6 | campaign = Campaign.create!(name: "Demo 1", description: "Description 1") 7 | emails = [ 8 | Email.create!(campaign: campaign, subject: Faker::Movie.title, body: Faker::Movie.quote), 9 | Email.create!(campaign: campaign, subject: Faker::Movie.title, body: Faker::Movie.quote) 10 | ] 11 | 12 | # create a complex data structure that embeds some active record objects 13 | data = { 14 | first_name: "John", 15 | last_name: "Doe", 16 | email: "john.doe@example.com", 17 | password: "Secret123ABC", 18 | corporate: { 19 | company: nil, 20 | details: {}, 21 | reports: [], 22 | campaigns: [campaign] 23 | }, 24 | emails: emails 25 | } 26 | 27 | # make some in-memory changes to campaign without saving them 28 | campaign.name = "Changed to Demo 2" 29 | campaign.description = "Changed to Description 2" 30 | 31 | # create the UID 32 | uid = URI::UID.build(data, include_blank: false, exclude: ["password"], include_changes: true) 33 | 34 | # add a binding.pry here if you want to introspect the UID 35 | # it's pretty interesting, so I highly recommend doing this 36 | # binding.pry 37 | 38 | # parse and decode the UID 39 | decoded = URI::UID.parse(uid.to_s).decode 40 | 41 | # dig out the campaign for some assertions 42 | decoded_campaign = decoded.dig(:corporate, :campaigns).first 43 | 44 | # this is the expected decoded data structure given the args passed to URI::UID.build 45 | expected = { 46 | first_name: "John", 47 | last_name: "Doe", 48 | email: "john.doe@example.com", 49 | corporate: { 50 | campaigns: [campaign] 51 | }, 52 | emails: emails 53 | } 54 | 55 | assert_equal expected, decoded 56 | 57 | assert campaign.changed? # in-memory campaign was changed, but not saved 58 | assert decoded_campaign.changed? # marshaled campaign should preserve the changes 59 | assert_equal campaign.changes, decoded_campaign.changes # changes should be identical 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/uri/uid_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class URI::UIDTest < Minitest::Test 4 | def test_from_payload 5 | expected = scalars 6 | payload = URI::UID.build(expected).payload 7 | actual = URI::UID.from_payload(payload).decode 8 | assert_equal expected, actual 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /universalid.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/universalid/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "universalid" 7 | s.version = UniversalID::VERSION 8 | s.authors = ["Nate Hopkins (hopsoft)"] 9 | s.email = ["natehop@gmail.com"] 10 | 11 | s.summary = "Fast, recursive, optimized, URL-Safe serialization for any Ruby object" 12 | s.description = <<~DESC 13 | Universal ID opens the flood gates with a deluge of profoundly powerful 14 | yet easily implemented new use-cases for your apps and scripts. 15 | DESC 16 | 17 | s.homepage = "https://github.com/hopsoft/universalid" 18 | s.license = "MIT" 19 | s.required_ruby_version = ">= 3.0" 20 | 21 | s.metadata["homepage_uri"] = s.homepage 22 | s.metadata["source_code_uri"] = s.homepage 23 | s.metadata["changelog_uri"] = s.homepage + "/blob/main/CHANGELOG.md" 24 | 25 | s.files = Dir.chdir(File.expand_path(__dir__)) do 26 | Dir["{config,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] 27 | end 28 | 29 | s.require_paths = ["lib"] 30 | 31 | s.add_dependency "activesupport", ">= 6.1" 32 | s.add_dependency "brotli", ">= 0.4" 33 | s.add_dependency "config", ">= 5.0" 34 | s.add_dependency "msgpack", ">= 1.7" 35 | s.add_dependency "zeitwerk", ">= 2.6" 36 | 37 | s.add_development_dependency "rails" 38 | s.add_development_dependency "amazing_print" 39 | s.add_development_dependency "faker" 40 | s.add_development_dependency "globalid", ">= 1.1" 41 | s.add_development_dependency "jbuilder" 42 | s.add_development_dependency "magic_frozen_string_literal" 43 | s.add_development_dependency "minitest-reporters" 44 | s.add_development_dependency "model_probe" 45 | s.add_development_dependency "oj" 46 | s.add_development_dependency "pry-byebug" 47 | s.add_development_dependency "pry-doc" 48 | s.add_development_dependency "rainbow" 49 | s.add_development_dependency "rake" 50 | s.add_development_dependency "simplecov" 51 | s.add_development_dependency "sqlite3" 52 | s.add_development_dependency "standard", ">= 1.32" 53 | s.add_development_dependency "timecop" 54 | end 55 | --------------------------------------------------------------------------------