├── .github ├── CODEOWNERS └── workflows │ ├── ci.yml │ └── gem-push.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── setup ├── gemfiles ├── Gemfile.rails-6.1-stable ├── Gemfile.rails-7.0-stable └── Gemfile.rails-7.2-stable ├── lib ├── polymorphic_integer_type.rb └── polymorphic_integer_type │ ├── activerecord_5_0_0 │ ├── association_query_handler_extension.rb │ └── polymorphic_array_value_extension.rb │ ├── belongs_to_polymorphic_association_extension.rb │ ├── extensions.rb │ ├── mapping.rb │ ├── module_generator.rb │ ├── polymorphic_foreign_association_extension.rb │ └── version.rb ├── polymorphic_integer_type.gemspec └── spec ├── polymorphic_integer_type_spec.rb ├── spec_helper.rb └── support ├── animal.rb ├── configuration.rb ├── database.yml ├── dog.rb ├── drink.rb ├── food.rb ├── link.rb ├── migrations ├── 1_create_link_table.rb ├── 2_create_animal_table.rb ├── 3_create_person_table.rb ├── 4_create_food_table.rb ├── 5_create_drink_table.rb ├── 6_create_plant_table.rb ├── 7_create_activity_table.rb ├── 8_create_profile_table.rb └── 9_create_profile_history_table.rb ├── namespaced_activity.rb ├── namespaced_animal.rb ├── namespaced_plant.rb ├── person.rb ├── profile.rb └── profile_history.rb /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /.github/workflows/ @clio/application-security @clio/penguins 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | gemfile: 17 | - Gemfile.rails-6.1-stable 18 | - Gemfile.rails-7.0-stable 19 | - Gemfile.rails-7.2-stable 20 | ruby-version: ['3.1', '3.0'] 21 | exclude: 22 | - gemfile: Gemfile.rails-7.2-stable 23 | ruby-version: "3.0" 24 | env: 25 | BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }} 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Set up Ruby ${{ matrix.ruby-version }} 29 | uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{ matrix.ruby-version }} 32 | - name: Install dependencies 33 | run: bundle install 34 | - name: Run tests 35 | run: 36 | bundle exec rspec 37 | -------------------------------------------------------------------------------- /.github/workflows/gem-push.yml: -------------------------------------------------------------------------------- 1 | name: Ruby Gem 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | build: 9 | name: Build + Publish 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Ruby 3.1 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: 3.1 20 | 21 | - name: Publish to RubyGems 22 | env: 23 | GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}" 24 | run: | 25 | mkdir -p $HOME/.gem 26 | touch $HOME/.gem/credentials 27 | chmod 0600 $HOME/.gem/credentials 28 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 29 | gem build *.gemspec 30 | gem push *.gem 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.gem 3 | *.rbc 4 | .bundle 5 | .config 6 | .yardoc 7 | Gemfile.lock 8 | InstalledFiles 9 | _yardoc 10 | coverage 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | spec/support/active_record.rb 17 | test/tmp 18 | test/version_tmp 19 | tmp 20 | .byebug_history 21 | polymorphic_integer_type_test 22 | gemfiles/*.lock 23 | .idea/ 24 | .ruby-version 25 | mysql 26 | polymorphic_integer_type_test-* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v3.2.1 (2023-12-14) 4 | 5 | ### Fixed 6 | 7 | - Not proper assigning polymorphic value with `has_many` and `has_one` reflection. 8 | 9 | ### Added 10 | 11 | - Added .idea/ folder to .gitignore 12 | 13 | ## v3.2.2 (2023-12-21) 14 | 15 | ### Fixed 16 | 17 | - Fixed polymorphic_foreign_association_extension.rb to be compatible with other reflection than `has_many` and `has_one`. 18 | 19 | ## v3.3.0 (2024-10-29) 20 | 21 | ### Changed 22 | 23 | - Upgrade rails support version to be compatible with 7.2 24 | 25 | ### Removed 26 | 27 | - Remove unsupported rails versions(5.0, 5.2, 6.0) and ruby version(2.7) 28 | 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in polymorphic_integer_type.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Kyle d'Oliveira 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PolymorphicIntegerType 2 | 3 | Rails' polymorphic associations are pretty useful. The example they give to set it up looks like: 4 | 5 | ```ruby 6 | class Picture < ActiveRecord::Base 7 | belongs_to :imageable, polymorphic: true 8 | end 9 | 10 | class Employee < ActiveRecord::Base 11 | has_many :pictures, as: :imageable 12 | end 13 | 14 | class Product < ActiveRecord::Base 15 | has_many :pictures, as: :imageable 16 | end 17 | ``` 18 | 19 | With a migration that looks like: 20 | 21 | ```ruby 22 | class CreatePictures < ActiveRecord::Migration 23 | def change 24 | create_table :pictures do |t| 25 | t.string :name 26 | t.integer :imageable_id 27 | t.string :imageable_type 28 | t.timestamps 29 | end 30 | end 31 | end 32 | ``` 33 | 34 | The problem with this approach is that `imageable_type` is a string (and by default it is 255 characters). This is a little ridiculous. For comparison, if we had a state machine with X states, would we describe the states with strings `"State1", "State2", etc` or would we just enumerate the state column and make it an integer? This gem will allow us to use an integer for the `imageable_type` column. 35 | 36 | ## Installation 37 | 38 | Add this line to your application's Gemfile: 39 | 40 | gem 'polymorphic_integer_type' 41 | 42 | And then execute: 43 | 44 | $ bundle 45 | 46 | Or install it yourself as: 47 | 48 | $ gem install polymorphic_integer_type 49 | 50 | ## Usage 51 | 52 | For the model where the `belongs_to` is defined, include `PolymorphicIntegerType::Extensions` and set the `polymorphic:` option to a hash that maps an integer stored in the database to the name of a Ruby class. 53 | 54 | ```ruby 55 | class Picture < ActiveRecord::Base 56 | include PolymorphicIntegerType::Extensions 57 | 58 | belongs_to :imageable, polymorphic: {1 => "Employee", 2 => "Product"} 59 | end 60 | ``` 61 | 62 | Next, include `PolymorphicIntegerType::Extensions` into any of the models that point back to the polymorphic integer type association (e.g., `Picture#imageable`) and add a [polymorphic association using `as:`](http://guides.rubyonrails.org/association_basics.html#polymorphic-associations). 63 | 64 | ```ruby 65 | class Employee < ActiveRecord::Base 66 | include PolymorphicIntegerType::Extensions 67 | 68 | has_many :pictures, as: :imageable 69 | end 70 | 71 | class Product < ActiveRecord::Base 72 | include PolymorphicIntegerType::Extensions 73 | 74 | has_many :pictures, as: :imageable 75 | end 76 | ``` 77 | 78 | ### External mappings 79 | 80 | You can also store polymorphic type mappings separate from your models. This should be loaded before the models. Putting it in an initializer is one way to do this (e.g., `config/initializers/polymorphic_type_mapping.rb`) 81 | 82 | ```ruby 83 | PolymorphicIntegerType::Mapping.configuration do |config| 84 | config.add :imageable, {1 => "Employee", 2 => "Product" } 85 | end 86 | ``` 87 | 88 | Note: The mapping here can start from whatever integer you wish, but I would advise not using 0. The reason being that if you had a new class, for instance `Avatar`, and also wanted to use this polymorphic association but forgot to include it in the mapping, it would effectively get `to_i` called on it and stored in the database. `"Avatar".to_i == 0`, so if your mapping included 0, this would create a weird bug. 89 | 90 | ### Migrating an existing association 91 | 92 | If you want to convert a polymorphic association that is already a string, you'll need to set up a migration. (Assuming SQL for the time being, but this should be pretty straightforward.) 93 | 94 | ```ruby 95 | class PictureToPolymorphicIntegerType < ActiveRecord::Migration 96 | 97 | def up 98 | change_table :pictures do |t| 99 | t.integer :new_imageable_type 100 | end 101 | 102 | execute <<-SQL 103 | UPDATE picture 104 | SET new_imageable_type = CASE imageable_type 105 | WHEN 'Employee' THEN 1 106 | WHEN 'Product' THEN 2 107 | END 108 | SQL 109 | 110 | change_table :pictures, :bulk => true do |t| 111 | t.remove :imageable_type 112 | t.rename :new_imageable_type, :imageable_type 113 | end 114 | end 115 | 116 | def down 117 | change_table :pictures do |t| 118 | t.string :new_imageable_type 119 | end 120 | 121 | execute <<-SQL 122 | UPDATE picture 123 | SET new_imageable_type = CASE imageable_type 124 | WHEN 1 THEN 'Employee' 125 | WHEN 2 THEN 'Product' 126 | END 127 | SQL 128 | 129 | change_table :pictures, :bulk => true do |t| 130 | t.remove :imageable_type 131 | t.rename :new_imageable_type, :imageable_type 132 | end 133 | end 134 | end 135 | ``` 136 | 137 | Lastly, you will need to be careful of any place where you are doing raw SQL queries with the string (`imageable_type = 'Employee'`). They should use the integer instead. 138 | 139 | ## Setup 140 | 141 | You'll need to have git, Ruby, and MySQL. Then get up and running with a few commands: 142 | 143 | ```bash 144 | $ git clone ... 145 | $ bundle install 146 | $ vim spec/support/database.yml # Update username and password 147 | $ bin/setup 148 | $ bundle exec rspec 149 | ``` 150 | 151 | ## Contributing 152 | 153 | 1. Fork it 154 | 2. Create your feature branch (`git checkout -b my-new-feature`) 155 | 3. Commit your changes (`git commit -am 'Add some feature'`) 156 | 4. Push to the branch (`git push origin my-new-feature`) 157 | 5. Create new Pull Request 158 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "yaml" 3 | require "active_record" 4 | 5 | namespace :test do 6 | task :all do 7 | Dir.glob("./gemfiles/Gemfile*").each do |gemfile| 8 | next if gemfile.end_with?(".lock") 9 | puts "Running specs for #{Pathname.new(gemfile).basename}" 10 | system("BUNDLE_GEMFILE=#{gemfile} bundle install > /dev/null && BUNDLE_GEMFILE=#{gemfile} bundle exec rspec") 11 | puts "" 12 | end 13 | end 14 | end 15 | 16 | namespace :db do 17 | database_config = YAML.load(File.open("./spec/support/database.yml")) 18 | admin_database_config = database_config.merge(database: "mysql") 19 | migration_path = File.expand_path("./spec/support/migrations") 20 | 21 | desc "Create the database" 22 | task :create do 23 | ActiveRecord::Base.establish_connection(admin_database_config) 24 | ActiveRecord::Base.connection.create_database(database_config.fetch(:database)) 25 | puts "Database created." 26 | end 27 | 28 | desc "Migrate the database" 29 | task :migrate do 30 | ActiveRecord::Base.establish_connection(database_config) 31 | ActiveRecord::Migrator.migrate(migration_path) 32 | Rake::Task["db:schema"].invoke 33 | puts "Database migrated." 34 | end 35 | 36 | desc "Drop the database" 37 | task :drop do 38 | ActiveRecord::Base.establish_connection(admin_database_config) 39 | ActiveRecord::Base.connection.drop_database(database_config.fetch(:database)) 40 | puts "Database deleted." 41 | end 42 | 43 | desc "Reset the database" 44 | task reset: [:drop, :create, :migrate] 45 | desc 'Create a db/schema.rb file that is portable against any DB supported by AR' 46 | 47 | task :schema do 48 | # Noop to make ActiveRecord happy 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | bundle exec rake db:drop db:create db:migrate 3 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-6.1-stable: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec path: ".." 6 | 7 | gem "activerecord", github: "rails/rails", branch: "6-1-stable" 8 | gem "sqlite3", "~> 1.4" 9 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-7.0-stable: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec path: ".." 6 | 7 | gem "activerecord", github: "rails/rails", branch: "7-0-stable" 8 | gem "sqlite3", "~> 1.4" 9 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails-7.2-stable: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec path: ".." 6 | 7 | gem "activerecord", github: "rails/rails", branch: "7-2-stable" 8 | -------------------------------------------------------------------------------- /lib/polymorphic_integer_type.rb: -------------------------------------------------------------------------------- 1 | ACTIVE_RECORD_VERSION = Gem::Version.new(ActiveRecord::VERSION::STRING) 2 | 3 | require "polymorphic_integer_type/version" 4 | require "polymorphic_integer_type/extensions" 5 | require "polymorphic_integer_type/mapping" 6 | require "polymorphic_integer_type/module_generator" 7 | require "polymorphic_integer_type/belongs_to_polymorphic_association_extension" 8 | require "polymorphic_integer_type/activerecord_5_0_0/polymorphic_array_value_extension" 9 | require "polymorphic_integer_type/polymorphic_foreign_association_extension" 10 | 11 | if ACTIVE_RECORD_VERSION < Gem::Version.new("5.2.0") 12 | require "polymorphic_integer_type/activerecord_5_0_0/association_query_handler_extension" 13 | end 14 | 15 | module PolymorphicIntegerType; end 16 | -------------------------------------------------------------------------------- /lib/polymorphic_integer_type/activerecord_5_0_0/association_query_handler_extension.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicIntegerType 2 | module AssociationQueryHandlerExtension 3 | def call(attribute, value) 4 | queries = {} 5 | table = value.associated_table 6 | 7 | if value.base_class 8 | queries[table.association_foreign_type.to_s] = polymorphic_value_for(value) 9 | end 10 | 11 | queries[table.association_foreign_key.to_s] = value.ids 12 | predicate_builder.build_from_hash(queries) 13 | end 14 | 15 | protected 16 | 17 | def polymorphic_value_for(query_value) 18 | table = query_value.associated_table 19 | association = table.send(:association) 20 | klass = association.active_record 21 | name = association.name 22 | 23 | if klass.respond_to?("#{name}_type_mapping") 24 | type_mapping = klass.send("#{name}_type_mapping") 25 | 26 | type_mapping.key(query_value.value.class.sti_name) || 27 | type_mapping.key(query_value.base_class.to_s) || 28 | type_mapping.key(query_value.base_class.sti_name) 29 | else 30 | query_value.base_class.name 31 | end 32 | end 33 | 34 | 35 | end 36 | end 37 | 38 | ActiveRecord::PredicateBuilder::AssociationQueryHandler.prepend(PolymorphicIntegerType::AssociationQueryHandlerExtension) 39 | -------------------------------------------------------------------------------- /lib/polymorphic_integer_type/activerecord_5_0_0/polymorphic_array_value_extension.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicIntegerType 2 | module PolymorphicArrayValueExtension 3 | 4 | # original method: 5 | # def type_to_ids_mapping 6 | # default_hash = Hash.new { |hsh, key| hsh[key] = [] } 7 | # result = values.each_with_object(default_hash) do |value, hash| 8 | # hash[klass(value).polymorphic_name] << convert_to_id(value) 9 | # end 10 | # end 11 | 12 | def type_to_ids_mapping 13 | if ACTIVE_RECORD_VERSION < Gem::Version.new("6.1") 14 | association = @associated_table.send(:association) 15 | else 16 | association = @associated_table.send(:reflection) 17 | end 18 | 19 | name = association.name 20 | default_hash = Hash.new { |hsh, key| hsh[key] = [] } 21 | values.each_with_object(default_hash) do |value, hash| 22 | klass = respond_to?(:klass, true) ? klass(value) : value.class 23 | if association.active_record.respond_to?("#{name}_type_mapping") 24 | mapping = association.active_record.send("#{name}_type_mapping") 25 | key ||= mapping.key(klass.polymorphic_name) if klass.respond_to?(:polymorphic_name) 26 | key ||= mapping.key(klass.sti_name) 27 | key ||= mapping.key(klass.base_class.to_s) 28 | key ||= mapping.key(klass.base_class.sti_name) 29 | 30 | hash[key] << convert_to_id(value) 31 | else 32 | hash[klass(value)&.polymorphic_name] << convert_to_id(value) 33 | end 34 | end 35 | end 36 | end 37 | end 38 | 39 | ActiveRecord::PredicateBuilder::PolymorphicArrayValue.prepend(PolymorphicIntegerType::PolymorphicArrayValueExtension) 40 | -------------------------------------------------------------------------------- /lib/polymorphic_integer_type/belongs_to_polymorphic_association_extension.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Associations 3 | class BelongsToPolymorphicAssociation < BelongsToAssociation 4 | private 5 | 6 | if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("6.1") 7 | def replace_keys(record) 8 | super 9 | owner[reflection.foreign_type] = record.class.base_class unless record.nil? 10 | end 11 | elsif 12 | def replace_keys(record, force: false) 13 | super 14 | owner[reflection.foreign_type] = record.class.base_class unless record.nil? 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/polymorphic_integer_type/extensions.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicIntegerType 2 | 3 | module Extensions 4 | module ClassMethods 5 | ActiveRecord::Reflection::HasManyReflection.attr_accessor(:foreign_integer_type) 6 | ActiveRecord::Reflection::HasManyReflection.attr_accessor(:integer_type) 7 | ActiveRecord::Reflection::HasOneReflection.attr_accessor(:foreign_integer_type) 8 | ActiveRecord::Reflection::HasOneReflection.attr_accessor(:integer_type) 9 | 10 | def belongs_to(name, scope = nil, **options) 11 | options = scope if scope.kind_of? Hash 12 | integer_type = options.delete :integer_type 13 | super 14 | if options[:polymorphic] && (integer_type || options[:polymorphic].is_a?(Hash)) 15 | mapping = 16 | case integer_type 17 | when true then PolymorphicIntegerType::Mapping[name] 18 | when nil then options[:polymorphic] 19 | else 20 | raise ArgumentError, "Unknown integer_type value: #{integer_type.inspect}" 21 | end.dup 22 | 23 | foreign_type = reflections[name.to_s].foreign_type 24 | _polymorphic_foreign_types << foreign_type 25 | 26 | # Required way to dynamically define a class method on the model 27 | define_singleton_method("#{foreign_type}_mapping") do 28 | mapping 29 | end 30 | 31 | ModuleGenerator.generate_and_include(self, foreign_type, name) 32 | 33 | validate do 34 | t = send(foreign_type) 35 | unless t.nil? || mapping.values.include?(t) 36 | errors.add(foreign_type, "is not included in the mapping") 37 | end 38 | end 39 | end 40 | end 41 | 42 | def remove_type_and_establish_mapping(name, options, scope) 43 | integer_type = options.delete :integer_type 44 | polymorphic_type_mapping = retrieve_polymorphic_type_mapping( 45 | polymorphic_type: options[:as], 46 | class_name: options[:class_name] || name.to_s.classify 47 | ) 48 | 49 | if options[:as] && (polymorphic_type_mapping || integer_type) 50 | poly_type = options.delete(:as) 51 | polymorphic_type_mapping ||= PolymorphicIntegerType::Mapping[poly_type] 52 | if polymorphic_type_mapping.nil? 53 | raise "Polymorphic type mapping missing for #{poly_type.inspect}" 54 | end 55 | 56 | klass_mapping = polymorphic_type_mapping.key(polymorphic_name) if respond_to?(:polymorphic_name) 57 | klass_mapping ||= polymorphic_type_mapping.key(sti_name) 58 | 59 | if klass_mapping.nil? 60 | raise "Class not found for #{inspect} in polymorphic type mapping: #{polymorphic_type_mapping}" 61 | end 62 | 63 | options[:foreign_key] ||= "#{poly_type}_id" 64 | foreign_type = options.delete(:foreign_type) || "#{poly_type}_type" 65 | 66 | options[:scope] ||= -> { 67 | condition = where(foreign_type => klass_mapping.to_i) 68 | condition = instance_exec(&scope).merge(condition) if scope.is_a?(Proc) 69 | condition 70 | } 71 | return foreign_type, klass_mapping.to_i 72 | else 73 | options[:scope] ||= scope 74 | return nil, nil 75 | end 76 | end 77 | 78 | def retrieve_polymorphic_type_mapping(polymorphic_type:, class_name:) 79 | return if polymorphic_type.nil? 80 | 81 | belongs_to_class = compute_type(class_name) 82 | method_name = "#{polymorphic_type}_type_mapping" 83 | 84 | if belongs_to_class && belongs_to_class.respond_to?(method_name) 85 | belongs_to_class.public_send(method_name) 86 | end 87 | end 88 | 89 | def has_many(name, scope = nil, **options, &extension) 90 | if scope.kind_of? Hash 91 | options = scope 92 | scope = nil 93 | end 94 | 95 | integer_type_values = remove_type_and_establish_mapping(name, options, scope) 96 | super(name, options.delete(:scope), **options, &extension).tap do 97 | remove_integer_type_and_set_attributes_and_extension(integer_type_values, reflections[name.to_s]) 98 | end 99 | end 100 | 101 | def has_one(name, scope = nil, **options) 102 | if scope.kind_of? Hash 103 | options = scope 104 | scope = nil 105 | end 106 | 107 | integer_type_values = remove_type_and_establish_mapping(name, options, scope) 108 | super(name, options.delete(:scope), **options).tap do 109 | remove_integer_type_and_set_attributes_and_extension(integer_type_values, reflections[name.to_s]) 110 | end 111 | end 112 | 113 | def remove_integer_type_and_set_attributes_and_extension(integer_type_values, reflection) 114 | foreign_integer_type = integer_type_values[0] 115 | integer_type = integer_type_values[1] 116 | is_polymorphic_integer = foreign_integer_type && integer_type 117 | 118 | if is_polymorphic_integer 119 | reflection.foreign_integer_type = foreign_integer_type 120 | reflection.integer_type = integer_type 121 | 122 | if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("6.1") 123 | ActiveRecord::Associations::Association.prepend(PolymorphicIntegerType::PolymorphicForeignAssociationExtension) 124 | else 125 | ActiveRecord::Associations::ForeignAssociation.prepend(PolymorphicIntegerType::PolymorphicForeignAssociationExtension) 126 | end 127 | end 128 | end 129 | 130 | 131 | end 132 | 133 | def self.included(base) 134 | base.class_eval { 135 | cattr_accessor :_polymorphic_foreign_types 136 | self._polymorphic_foreign_types = [] 137 | } 138 | base.extend(ClassMethods) 139 | end 140 | 141 | def _polymorphic_foreign_types 142 | self.class._polymorphic_foreign_types 143 | end 144 | 145 | def [](value) 146 | if _polymorphic_foreign_types.include?(value) 147 | send(value) 148 | else 149 | super(value) 150 | end 151 | end 152 | 153 | def []=(attr_name, value) 154 | if _polymorphic_foreign_types.include?(attr_name) 155 | send("#{attr_name}=", value) 156 | else 157 | super(attr_name, value) 158 | end 159 | end 160 | 161 | end 162 | 163 | end 164 | -------------------------------------------------------------------------------- /lib/polymorphic_integer_type/mapping.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicIntegerType 2 | 3 | class Mapping 4 | @@mapping = {} 5 | 6 | def self.configuration 7 | yield(self) 8 | end 9 | 10 | def self.add(as, mapping) 11 | @@mapping[as] = mapping 12 | end 13 | 14 | def self.[](as) 15 | @@mapping[as] || {} 16 | end 17 | 18 | singleton_class.send(:alias_method, :[]=, :add) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/polymorphic_integer_type/module_generator.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicIntegerType 2 | class ModuleGenerator 3 | def self.generate_and_include(klass,foreign_type, name) 4 | foreign_type_extension = Module.new do 5 | define_method foreign_type do 6 | t = super() 7 | self.class.send("#{foreign_type}_mapping")[t] 8 | end 9 | 10 | define_method "#{foreign_type}=" do |klass| 11 | mapping = self.class.send("#{foreign_type}_mapping") 12 | enum = mapping.key(klass.to_s) 13 | if klass.kind_of?(Class) && klass <= ActiveRecord::Base 14 | enum ||= mapping.key(klass.polymorphic_name) if klass.respond_to?(:polymorphic_name) 15 | enum ||= mapping.key(klass.sti_name) 16 | enum ||= mapping.key(klass.base_class.to_s) 17 | enum ||= mapping.key(klass.base_class.sti_name) 18 | end 19 | enum ||= klass if klass != NilClass 20 | super(enum) 21 | end 22 | 23 | define_method "#{name}=" do |record| 24 | super(record) 25 | send("#{foreign_type}=", record.class) 26 | association(name).loaded! 27 | end 28 | end 29 | 30 | klass.include(foreign_type_extension) 31 | end 32 | end 33 | end 34 | 35 | -------------------------------------------------------------------------------- /lib/polymorphic_integer_type/polymorphic_foreign_association_extension.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicIntegerType 2 | module PolymorphicForeignAssociationExtension 3 | 4 | def set_owner_attributes(record) 5 | super 6 | if reflection.try(:foreign_integer_type) && reflection.try(:integer_type) 7 | record._write_attribute(reflection.foreign_integer_type, reflection.integer_type) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/polymorphic_integer_type/version.rb: -------------------------------------------------------------------------------- 1 | module PolymorphicIntegerType 2 | VERSION = "3.3.0" 3 | end 4 | -------------------------------------------------------------------------------- /polymorphic_integer_type.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'polymorphic_integer_type/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "polymorphic_integer_type" 8 | spec.version = PolymorphicIntegerType::VERSION 9 | spec.authors = ["Kyle d'Oliveira"] 10 | spec.email = ["kyle@goclio.com"] 11 | spec.description = %q{Allows the *_type field in the DB to be an integer rather than a string} 12 | spec.summary = %q{Use integers rather than strings for the _type field} 13 | spec.homepage = "" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -- . ':!.github/'`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "activerecord", "< 8" 22 | spec.add_development_dependency "bundler" 23 | spec.add_development_dependency "rake" 24 | spec.add_development_dependency "rspec" 25 | spec.add_development_dependency "sqlite3" 26 | spec.add_development_dependency "pry-byebug" 27 | end 28 | -------------------------------------------------------------------------------- /spec/polymorphic_integer_type_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PolymorphicIntegerType do 4 | let(:owner) { Person.create(name: "Kyle") } 5 | let(:dog) { Animal.create(name: "Bela", kind: "Dog", owner: owner) } 6 | let(:cat) { Animal.create(name: "Alexi", kind: "Cat") } 7 | 8 | let(:kibble) { Food.create(name: "Kibble") } 9 | let(:chocolate) { Food.create(name: "Choclate") } 10 | 11 | let(:milk) { Drink.create(name: "milk") } 12 | let(:water) { Drink.create(name: "Water") } 13 | let(:whiskey) { Drink.create(name: "Whiskey") } 14 | 15 | let(:link) { Link.create(source: source, target: target) } 16 | 17 | 18 | context "when creating associations" do 19 | it "sets the source_type" do 20 | link = dog.source_links.new 21 | expect(link.source_type).to eq("Animal") 22 | end 23 | 24 | it "sets the target_type" do 25 | link = kibble.target_links.new 26 | expect(link.target_type).to eq("Food") 27 | end 28 | 29 | context "from HasManyReflection" do 30 | it "sets the source properly HasManyReflection" do 31 | link_1 = Link.create() 32 | link_2 = Link.create() 33 | dog.source_links = [link_1, link_2] 34 | expect(link_1.source_type).to eq("Animal") 35 | expect(link_1.source_id).to eq(dog.id) 36 | expect(link_2.source_type).to eq("Animal") 37 | expect(link_1.source_id).to eq(dog.id) 38 | end 39 | end 40 | 41 | context "from HasOneReflection" do 42 | it "sets the source properly HasOneReflection" do 43 | link = Link.create() 44 | dog.source_link = link 45 | 46 | expect(link.source_type).to eq("Animal") 47 | expect(link.source_id).to eq(dog.id) 48 | end 49 | end 50 | 51 | context "when models are namespaced" do 52 | context "and mappings include namespaces" do 53 | it "sets the source_type" do 54 | allow(Link).to receive(:source_type_mapping).and_return({3 => "Namespaced::Plant"}) 55 | allow(Link).to receive(:source_type_mapping2).and_return({3 => "Namespaced::Plant"}) 56 | 57 | link = Namespaced::Plant.create(name: "Oak").source_links.new 58 | expect(link.source_type).to eq("Namespaced::Plant") 59 | end 60 | 61 | it "sets the target_type" do 62 | allow(Link).to receive(:target_type_mapping).and_return({3 => "Namespaced::Activity"}) 63 | link = Namespaced::Activity.create(name: "swaying").target_links.new 64 | expect(link.target_type).to eq("Namespaced::Activity") 65 | end 66 | end 67 | 68 | context "and mappings don't include namespaces" do 69 | it "sets the source type" do 70 | Link.source_type_mapping 71 | link = Namespaced::Plant.create(name: "Oak").source_links.new 72 | expect(link.source_type).to eq("Plant") 73 | end 74 | 75 | it "sets the target type" do 76 | link = Namespaced::Activity.create(name:"swaying").target_links.new 77 | expect(link.target_type).to eq("Activity") 78 | end 79 | end 80 | end 81 | end 82 | 83 | context "when the source is nil" do 84 | let(:source) { nil } 85 | let(:target) { nil } 86 | it "should have the no id/type for the source" do 87 | expect(link.source_id).to be_nil 88 | expect(link.source_type).to be_nil 89 | expect(link.source).to be_nil 90 | end 91 | end 92 | 93 | context "when the source is a class that modifies the sti_name or polymorphic_name" do 94 | context "and we leverage the polymorphic_name" do 95 | before do 96 | allow(PolymorphicIntegerType).to receive(:use_polymorphic_name).and_return(true) 97 | end 98 | 99 | it "properly sets the source_type to the modified class name" do 100 | link = Link.new(source: Namespaced::Animal.new) 101 | expect(link.source_type).to eql "Animal" 102 | end 103 | 104 | it "can read dirty attributes from an associated object" do 105 | animal = Namespaced::Animal.create!(name: "Oldie") 106 | animal.name = "Newton" 107 | link = Link.create!(source: animal) 108 | 109 | expect(link.source.name).to eq("Newton") 110 | end 111 | end 112 | 113 | it "properly sets the source_type to the modified class name" do 114 | link = Link.new(source: Namespaced::Animal.new) 115 | expect(link.source_type).to eql "Animal" 116 | end 117 | 118 | it "can read dirty attributes from an associated object" do 119 | animal = Namespaced::Animal.create!(name: "Oldie") 120 | animal.name = "Newton" 121 | link = Link.create!(source: animal) 122 | 123 | expect(link.source.name).to eq("Newton") 124 | end 125 | end 126 | 127 | context "when querying the associations" do 128 | let(:source) { cat } 129 | let(:target) { nil } 130 | 131 | it "properly finds the object with a where" do 132 | expect(Link.where(source: source, id: link.id).first).to eql link 133 | end 134 | 135 | it "properly finds the object when passing an array of sources" do 136 | expect(Link.where(source: [source])).to eq [link] 137 | end 138 | 139 | it "properly finds the object with a find_by" do 140 | expect(Link.find_by(source: source, id: link.id)).to eql link 141 | end 142 | 143 | context "when source and target are namespaced without modifying polymorphic_name" do 144 | it "properly finds the object" do 145 | plant = Namespaced::Plant.create(name: "Mighty", kind: "Oak", owner: owner) 146 | activity = Namespaced::Activity.create(name: "swaying") 147 | link = Link.create(source: plant, target: activity) 148 | expect(Link.where(source: plant, id: link.id).first).to eql link 149 | end 150 | end 151 | end 152 | 153 | shared_examples "proper source" do 154 | it "should have the proper id, type and object for the source" do 155 | expect(link.source_id).to eql source.id 156 | expect(link.source_type).to eql source.class.to_s 157 | expect(link.source).to eql source 158 | end 159 | end 160 | 161 | shared_examples "proper target" do 162 | it "should have the proper id, type and object for the target" do 163 | expect(link.target_id).to eql target.id 164 | expect(link.target_type).to eql target.class.to_s 165 | expect(link.target).to eql target 166 | end 167 | end 168 | 169 | context "When a link is created through an association" do 170 | let(:link) { source.source_links.create } 171 | let(:source) { cat } 172 | include_examples "proper source" 173 | 174 | context "and the link is accessed through the associations" do 175 | before { link } 176 | 177 | it "should have the proper source" do 178 | expect(source.reload.source_links[0].source).to eql source 179 | end 180 | end 181 | end 182 | context "When a link is given polymorphic record" do 183 | let(:link) { Link.create(source: source) } 184 | let(:source) { cat } 185 | include_examples "proper source" 186 | 187 | context "and when it already has a polymorphic record" do 188 | let(:target) { kibble } 189 | before { link.update(target: target) } 190 | 191 | include_examples "proper source" 192 | include_examples "proper target" 193 | end 194 | end 195 | 196 | context "When a link is given polymorphic id and type" do 197 | let(:link) { Link.create(source_id: source.id, source_type: source.class.to_s) } 198 | let(:source) { cat } 199 | include_examples "proper source" 200 | 201 | context "and when it already has a polymorphic id and type" do 202 | let(:target) { kibble } 203 | before { link.update(target_id: target.id, target_type: target.class.to_s) } 204 | include_examples "proper source" 205 | include_examples "proper target" 206 | end 207 | end 208 | 209 | context "When using a relation to the links with eager loading" do 210 | let!(:links){ 211 | [Link.create(source: source, target: kibble), 212 | Link.create(source: source, target: water)] 213 | } 214 | let(:source) { cat } 215 | 216 | it "should be able to return the links and the targets" do 217 | expect(cat.source_links).to match_array links 218 | expect(cat.source_links.includes(:target).collect(&:target)).to match_array [water, kibble] 219 | end 220 | end 221 | 222 | context "When using a through relation to the links with eager loading" do 223 | let!(:links){ 224 | [Link.create(source: source, target: kibble), 225 | Link.create(source: source, target: water)] 226 | } 227 | let(:source) { dog } 228 | 229 | it "should be able to return the links and the targets" do 230 | expect(owner.pet_source_links).to match_array links 231 | expect(owner.pet_source_links.includes(:target).collect(&:target)).to match_array [water, kibble] 232 | end 233 | end 234 | 235 | context "When eager loading the polymorphic association" do 236 | let(:link) { Link.create(source_id: source.id, source_type: source.class.to_s) } 237 | let(:source) { cat } 238 | 239 | context "and when there are multiples sources" do 240 | let(:link_2) { Link.create(source_id: source_2.id, source_type: source_2.class.to_s) } 241 | let(:source_2) { dog } 242 | it "should be able to preload both associations" do 243 | links = Link.includes(:source).where(id: [link.id, link_2.id]).order(:id) 244 | expect(links.first.source).to eql cat 245 | expect(links.last.source).to eql dog 246 | end 247 | end 248 | 249 | it "should be able to preload the association" do 250 | l = Link.includes(:source).where(id: link.id).first 251 | expect(l.source).to eql cat 252 | end 253 | end 254 | 255 | context "when the association is an STI table" do 256 | let(:link) { Link.create(source: source, target: whiskey) } 257 | let(:source) { Dog.create(name: "Bela", kind: "Dog", owner: owner) } 258 | it "should have the proper id, type and object for the source" do 259 | expect(link.source_id).to eql source.id 260 | expect(link.source_type).to eql "Animal" 261 | expect(link.source).to eql source 262 | end 263 | end 264 | 265 | context "when mapping is given inline in the belongs_to model" do 266 | class InlineLink < ActiveRecord::Base 267 | include PolymorphicIntegerType::Extensions 268 | 269 | self.table_name = "links" 270 | 271 | belongs_to :source, polymorphic: {10 => "Person", 11 => "InlineAnimal"} 272 | belongs_to :target, polymorphic: {10 => "Food", 13 => "InlineDrink"} 273 | belongs_to :normal_target, polymorphic: true 274 | end 275 | 276 | class InlineAnimal < ActiveRecord::Base 277 | include PolymorphicIntegerType::Extensions 278 | 279 | self.table_name = "animals" 280 | 281 | belongs_to :owner, class_name: "Person" 282 | has_many :source_links, as: :source, class_name: "InlineLink" 283 | end 284 | 285 | class InlineDrink < ActiveRecord::Base 286 | include PolymorphicIntegerType::Extensions 287 | 288 | self.table_name = "drinks" 289 | 290 | has_many :inline_links, as: :target 291 | end 292 | 293 | let!(:animal) { InlineAnimal.create!(name: "Lucy") } 294 | let!(:drink) { InlineDrink.create!(name: "Water") } 295 | let!(:link) { InlineLink.create!(source: animal, target: drink, normal_target: drink) } 296 | 297 | let(:source) { animal } 298 | let(:target) { drink } 299 | 300 | include_examples "proper source" 301 | include_examples "proper target" 302 | 303 | it "creates foreign_type mapping method" do 304 | expect(Link.source_type_mapping).to eq({1 => "Person", 2 => "Animal", 3 => "Plant"}) 305 | expect(InlineLink.source_type_mapping).to eq({10 => "Person", 11 => "InlineAnimal"}) 306 | end 307 | 308 | it "pulls mapping from given hash" do 309 | expect(link.source_id).to eq(animal.id) 310 | expect(link[:source_type]).to eq(11) 311 | expect(link.target_id).to eq(drink.id) 312 | expect(link[:target_type]).to eq(13) 313 | end 314 | 315 | it "doesn't break string type polymorphic associations" do 316 | expect(link.normal_target).to eq(drink) 317 | expect(link.normal_target_type).to eq("InlineDrink") 318 | end 319 | end 320 | 321 | context "when mapping assigned to `polymorphic` option on belongs_to model" do 322 | class InlineLink2 < ActiveRecord::Base 323 | include PolymorphicIntegerType::Extensions 324 | 325 | self.table_name = "links" 326 | 327 | belongs_to :source, polymorphic: {10 => "Person", 11 => "InlineAnimal2"} 328 | belongs_to :target, polymorphic: {10 => "Food", 13 => "InlineDrink2"} 329 | belongs_to :normal_target, polymorphic: true 330 | end 331 | 332 | class InlineAnimal2 < ActiveRecord::Base 333 | include PolymorphicIntegerType::Extensions 334 | 335 | self.table_name = "animals" 336 | 337 | has_many :source_links, as: :source, class_name: "InlineLink2" 338 | end 339 | 340 | class InlineDrink2 < ActiveRecord::Base 341 | include PolymorphicIntegerType::Extensions 342 | 343 | self.table_name = "drinks" 344 | 345 | has_many :inline_link2s, as: :target 346 | end 347 | 348 | let!(:animal) { InlineAnimal2.create!(name: "Lucy") } 349 | let!(:drink) { InlineDrink2.create!(name: "Water") } 350 | let!(:link) { InlineLink2.create!(source: animal, target: drink, normal_target: drink) } 351 | 352 | let(:source) { animal } 353 | let(:target) { drink } 354 | 355 | include_examples "proper source" 356 | include_examples "proper target" 357 | 358 | it "pulls mapping from given hash" do 359 | expect(link.source_id).to eq(animal.id) 360 | expect(link[:source_type]).to eq(11) 361 | expect(link.target_id).to eq(drink.id) 362 | expect(link[:target_type]).to eq(13) 363 | end 364 | 365 | it "pulls mapping from given hash" do 366 | animal.source_links.new 367 | end 368 | 369 | it "doesn't break string type polymorphic associations" do 370 | expect(link.normal_target).to eq(drink) 371 | expect(link.normal_target_type).to eq("InlineDrink2") 372 | end 373 | end 374 | 375 | context "when using other reflection" do 376 | it "owner able to association ActiveRecord::Reflection::ThroughReflection successfully" do 377 | profile_history = ProfileHistory.new 378 | owner.profile_histories << profile_history 379 | 380 | expect(owner.profile_histories).to eq([profile_history]) 381 | end 382 | end 383 | end 384 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'active_record' 3 | require 'polymorphic_integer_type' 4 | require 'support/configuration' 5 | require 'support/link' 6 | require 'support/animal' 7 | require 'support/namespaced_animal' 8 | require 'support/namespaced_plant' 9 | require 'support/dog' 10 | require 'support/person' 11 | require 'support/food' 12 | require 'support/drink' 13 | require 'support/profile' 14 | require 'support/profile_history' 15 | require 'support/namespaced_activity' 16 | require 'byebug' 17 | require 'pry' 18 | 19 | RSpec.configure do |config| 20 | config.before(:suite) do 21 | database_config = YAML.load(File.open("#{File.dirname(__FILE__)}/support/database.yml")) 22 | migrations_path = "#{File.dirname(__FILE__)}/support/migrations" 23 | active_record_version = Gem::Version.new(ActiveRecord::VERSION::STRING) 24 | 25 | ActiveRecord::Base.establish_connection(database_config) 26 | 27 | if active_record_version >= Gem::Version.new("6.1") && active_record_version < Gem::Version.new("7.0") 28 | ActiveRecord::MigrationContext.new(migrations_path, ActiveRecord::SchemaMigration).migrate 29 | end 30 | 31 | if active_record_version >= Gem::Version.new("7.0") 32 | ActiveRecord::MigrationContext.new(migrations_path).migrate 33 | end 34 | end 35 | 36 | config.around do |example| 37 | ActiveRecord::Base.transaction do 38 | example.run 39 | raise ActiveRecord::Rollback 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/support/animal.rb: -------------------------------------------------------------------------------- 1 | class Animal < ActiveRecord::Base 2 | include PolymorphicIntegerType::Extensions 3 | 4 | belongs_to :owner, class_name: "Person" 5 | has_many :source_links, as: :source, integer_type: true, class_name: "Link" 6 | has_one :source_link, as: :source, integer_type: true, class_name: "Link" 7 | end -------------------------------------------------------------------------------- /spec/support/configuration.rb: -------------------------------------------------------------------------------- 1 | PolymorphicIntegerType::Mapping.configuration do |config| 2 | config.add :source, {1 => "Person", 2 => "Animal", 3 => "Plant"} 3 | config.add :target, {1 => "Food", 2 => "Drink", 3 => "Activity"} 4 | end 5 | -------------------------------------------------------------------------------- /spec/support/database.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :adapter: sqlite3 3 | :host: localhost 4 | :database: polymorphic_integer_type_test 5 | :username: root 6 | :password: '' 7 | -------------------------------------------------------------------------------- /spec/support/dog.rb: -------------------------------------------------------------------------------- 1 | class Dog < Animal 2 | end 3 | -------------------------------------------------------------------------------- /spec/support/drink.rb: -------------------------------------------------------------------------------- 1 | class Drink < ActiveRecord::Base 2 | include PolymorphicIntegerType::Extensions 3 | 4 | has_many :target_links, as: :target, integer_type: true, class_name: "Link" 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/food.rb: -------------------------------------------------------------------------------- 1 | class Food < ActiveRecord::Base 2 | include PolymorphicIntegerType::Extensions 3 | 4 | has_many :target_links, as: :target, integer_type: true, class_name: "Link" 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/link.rb: -------------------------------------------------------------------------------- 1 | class Link < ActiveRecord::Base 2 | include PolymorphicIntegerType::Extensions 3 | 4 | belongs_to :source, polymorphic: true, integer_type: true 5 | belongs_to :target, polymorphic: true, integer_type: true 6 | end 7 | -------------------------------------------------------------------------------- /spec/support/migrations/1_create_link_table.rb: -------------------------------------------------------------------------------- 1 | class CreateLinkTable < ActiveRecord::Migration[5.0] 2 | 3 | def up 4 | create_table :links do |t| 5 | t.integer :target_id 6 | t.integer :target_type 7 | t.integer :normal_target_id 8 | t.string :normal_target_type 9 | t.integer :source_id 10 | t.integer :source_type 11 | end 12 | end 13 | 14 | def down 15 | drop_table :links 16 | end 17 | 18 | end 19 | 20 | 21 | -------------------------------------------------------------------------------- /spec/support/migrations/2_create_animal_table.rb: -------------------------------------------------------------------------------- 1 | class CreateAnimalTable < ActiveRecord::Migration[5.0] 2 | 3 | def up 4 | create_table :animals do |t| 5 | t.string :name 6 | t.string :type 7 | t.string :kind 8 | t.integer :owner_id 9 | end 10 | end 11 | 12 | def down 13 | drop_table :animals 14 | end 15 | 16 | end 17 | 18 | 19 | -------------------------------------------------------------------------------- /spec/support/migrations/3_create_person_table.rb: -------------------------------------------------------------------------------- 1 | class CreatePersonTable < ActiveRecord::Migration[5.0] 2 | 3 | def up 4 | create_table :people do |t| 5 | t.string :name 6 | end 7 | end 8 | 9 | def down 10 | drop_table :people 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/migrations/4_create_food_table.rb: -------------------------------------------------------------------------------- 1 | class CreateFoodTable < ActiveRecord::Migration[5.0] 2 | 3 | def up 4 | create_table :foods do |t| 5 | t.string :name 6 | end 7 | end 8 | 9 | def down 10 | drop_table :foods 11 | end 12 | 13 | end 14 | 15 | 16 | -------------------------------------------------------------------------------- /spec/support/migrations/5_create_drink_table.rb: -------------------------------------------------------------------------------- 1 | class CreateDrinkTable < ActiveRecord::Migration[5.0] 2 | 3 | def up 4 | create_table :drinks do |t| 5 | t.string :name 6 | end 7 | end 8 | 9 | def down 10 | drop_table :drinks 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/migrations/6_create_plant_table.rb: -------------------------------------------------------------------------------- 1 | class CreatePlantTable < ActiveRecord::Migration[5.0] 2 | 3 | def up 4 | create_table :plants do |t| 5 | t.string :name 6 | t.string :type 7 | t.string :kind 8 | t.integer :owner_id 9 | end 10 | end 11 | 12 | def down 13 | drop_table :plants 14 | end 15 | 16 | end 17 | 18 | -------------------------------------------------------------------------------- /spec/support/migrations/7_create_activity_table.rb: -------------------------------------------------------------------------------- 1 | class CreateActivityTable < ActiveRecord::Migration[5.0] 2 | 3 | def up 4 | create_table :activities do |t| 5 | t.string :name 6 | end 7 | end 8 | 9 | def down 10 | drop_table :activities 11 | end 12 | 13 | end 14 | 15 | 16 | -------------------------------------------------------------------------------- /spec/support/migrations/8_create_profile_table.rb: -------------------------------------------------------------------------------- 1 | class CreateProfileTable < ActiveRecord::Migration[5.0] 2 | 3 | def up 4 | create_table :profiles do |t| 5 | t.integer :person_id 6 | t.integer :profile_history_id 7 | end 8 | end 9 | 10 | def down 11 | drop_table :profiles 12 | end 13 | 14 | end 15 | 16 | 17 | -------------------------------------------------------------------------------- /spec/support/migrations/9_create_profile_history_table.rb: -------------------------------------------------------------------------------- 1 | class CreateProfileHistoryTable < ActiveRecord::Migration[5.0] 2 | 3 | def up 4 | create_table :profile_histories do |t| 5 | end 6 | end 7 | 8 | def down 9 | drop_table :profile_histories 10 | end 11 | 12 | end 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/support/namespaced_activity.rb: -------------------------------------------------------------------------------- 1 | module Namespaced 2 | class Activity < ActiveRecord::Base 3 | include PolymorphicIntegerType::Extensions 4 | 5 | self.store_full_sti_class = false 6 | self.table_name = "activities" 7 | 8 | has_many :target_links, as: :target, inverse_of: :target, integer_type: true, class_name: "Link" 9 | end 10 | end 11 | 12 | -------------------------------------------------------------------------------- /spec/support/namespaced_animal.rb: -------------------------------------------------------------------------------- 1 | module Namespaced 2 | class Animal < ActiveRecord::Base 3 | 4 | self.store_full_sti_class = false 5 | self.table_name = "animals" 6 | 7 | def self.polymorphic_name 8 | "Animal" 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/namespaced_plant.rb: -------------------------------------------------------------------------------- 1 | module Namespaced 2 | class Plant < ActiveRecord::Base 3 | include PolymorphicIntegerType::Extensions 4 | 5 | self.store_full_sti_class = false 6 | self.table_name = "plants" 7 | 8 | belongs_to :owner, class_name: "Person" 9 | has_many :source_links, as: :source, inverse_of: :source, integer_type: true, class_name: "Link" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/person.rb: -------------------------------------------------------------------------------- 1 | class Person < ActiveRecord::Base 2 | include PolymorphicIntegerType::Extensions 3 | 4 | has_many :pets, class_name: "Animal", foreign_key: :owner_id 5 | has_many :source_links, as: :source, integer_type: true, class_name: "Link" 6 | 7 | has_many :pet_source_links, class_name: "Link", through: :pets, source: :source_links 8 | has_many :profiles 9 | has_many :profile_histories, class_name: "ProfileHistory", through: :profiles 10 | end 11 | -------------------------------------------------------------------------------- /spec/support/profile.rb: -------------------------------------------------------------------------------- 1 | class Profile < ActiveRecord::Base 2 | belongs_to :person 3 | belongs_to :profile_history 4 | end 5 | -------------------------------------------------------------------------------- /spec/support/profile_history.rb: -------------------------------------------------------------------------------- 1 | class ProfileHistory < ActiveRecord::Base 2 | has_many :profiles 3 | end 4 | --------------------------------------------------------------------------------