Skip to content

Commit bae9da7

Browse files
author
Kyle d'Oliveira
authored
Merge pull request #18 from jnraine/master
Add inline mappings and simplify configuration
2 parents e37bbee + 98c81ef commit bae9da7

17 files changed

Lines changed: 262 additions & 98 deletions

README.md

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# PolymorphicIntegerType
22

33
Rails' polymorphic associations are pretty useful. The example they give to set it up looks like:
4+
45
```ruby
56
class Picture < ActiveRecord::Base
67
belongs_to :imageable, polymorphic: true
@@ -16,6 +17,7 @@ end
1617
```
1718

1819
With a migration that looks like:
20+
1921
```ruby
2022
class CreatePictures < ActiveRecord::Migration
2123
def change
@@ -49,38 +51,48 @@ For Rails 3.2 use version < 2. Version >= 2 has been tested on Rails 4.2 and Rub
4951

5052
## Usage
5153

52-
The gem is pretty straightforward to use.
54+
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.
5355

54-
First, include the extensions module and add the `integer_type` option to the associations that are going to be using this. (That way it will play nicely with polymorphic associations whose type you would rather leave as a string.)
5556
```ruby
5657
class Picture < ActiveRecord::Base
5758
include PolymorphicIntegerType::Extensions
58-
belongs_to :imageable, polymorphic: true, integer_type: true
59+
60+
belongs_to :imageable, polymorphic: {1 => "Employee", 2 => "Product"}
5961
end
62+
```
6063

64+
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).
65+
66+
```ruby
6167
class Employee < ActiveRecord::Base
6268
include PolymorphicIntegerType::Extensions
63-
has_many :pictures, as: :imageable, integer_type: true
69+
70+
has_many :pictures, as: :imageable
6471
end
6572

6673
class Product < ActiveRecord::Base
6774
include PolymorphicIntegerType::Extensions
68-
has_many :pictures, as: :imageable, integer_type: true
75+
76+
has_many :pictures, as: :imageable
6977
end
7078
```
7179

72-
Second, you need to create a mapping for the polymorphic associations. This should be loaded before the models. Putting it in an initializer is good (`config/initializers/polymorphic_type_mapping.rb`)
73-
```ruby
74-
PolymorphicIntegerType::Mapping.configuration do |config|
80+
### External mappings
7581

76-
config.add :imageable, {1 => "Employee", 2 => "Product" }
82+
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`)
7783

84+
```ruby
85+
PolymorphicIntegerType::Mapping.configuration do |config|
86+
config.add :imageable, {1 => "Employee", 2 => "Product" }
7887
end
7988
```
8089

8190
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.
8291

92+
### Migrating an existing association
93+
8394
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.)
95+
8496
```ruby
8597
class PictureToPolymorphicIntegerType < ActiveRecord::Migration
8698

@@ -125,8 +137,18 @@ end
125137
```
126138

127139
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.
128-
129140

141+
## Setup
142+
143+
You'll need to have git, Ruby, and MySQL. Then get up and running with a few commands:
144+
145+
```bash
146+
$ git clone ...
147+
$ bundle install
148+
$ vim spec/support/database.yml # Update username and password
149+
$ bin/setup
150+
$ bundle exec rspec
151+
```
130152

131153
## Contributing
132154

Rakefile

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,39 @@
11
require "bundler/gem_tasks"
2+
require "yaml"
3+
require "active_record"
4+
5+
namespace :db do
6+
database_config = YAML.load(File.open("./spec/support/database.yml"))
7+
admin_database_config = database_config.merge(database: "mysql")
8+
migration_path = File.expand_path("./spec/support/migrations")
9+
10+
desc "Create the database"
11+
task :create do
12+
ActiveRecord::Base.establish_connection(admin_database_config)
13+
ActiveRecord::Base.connection.create_database(database_config.fetch(:database))
14+
puts "Database created."
15+
end
16+
17+
desc "Migrate the database"
18+
task :migrate do
19+
ActiveRecord::Base.establish_connection(database_config)
20+
ActiveRecord::Migrator.migrate(migration_path)
21+
Rake::Task["db:schema"].invoke
22+
puts "Database migrated."
23+
end
24+
25+
desc "Drop the database"
26+
task :drop do
27+
ActiveRecord::Base.establish_connection(admin_database_config)
28+
ActiveRecord::Base.connection.drop_database(database_config.fetch(:database))
29+
puts "Database deleted."
30+
end
31+
32+
desc "Reset the database"
33+
task reset: [:drop, :create, :migrate]
34+
desc 'Create a db/schema.rb file that is portable against any DB supported by AR'
35+
36+
task :schema do
37+
# Noop to make ActiveRecord happy
38+
end
39+
end

bin/setup

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/bash
2+
bundle exec rake db:drop db:create db:migrate

lib/polymorphic_integer_type/extensions.rb

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,22 @@ def belongs_to(name, scope = nil, options = {})
77
options = scope if scope.kind_of? Hash
88
integer_type = options.delete :integer_type
99
super
10-
if options[:polymorphic] && integer_type
11-
mapping = PolymorphicIntegerType::Mapping[name]
10+
if options[:polymorphic] && (integer_type || options[:polymorphic].is_a?(Hash))
11+
mapping =
12+
case integer_type
13+
when true then PolymorphicIntegerType::Mapping[name]
14+
when nil then options[:polymorphic]
15+
else
16+
raise ArgumentError, "Unknown integer_type value: #{integer_type.inspect}"
17+
end.dup
18+
1219
foreign_type = reflections[name.to_s].foreign_type
13-
self._polymorphic_foreign_types << foreign_type
20+
_polymorphic_foreign_types << foreign_type
21+
22+
# Required way to dynamically define a class method on the model
23+
singleton_class.__send__(:define_method, "#{foreign_type}_mapping") do
24+
mapping
25+
end
1426

1527
define_method foreign_type do
1628
t = super()
@@ -40,11 +52,23 @@ def belongs_to(name, scope = nil, options = {})
4052

4153
def remove_type_and_establish_mapping(name, options, scope)
4254
integer_type = options.delete :integer_type
43-
if options[:as] && integer_type
55+
polymorphic_type_mapping = retrieve_polymorphic_type_mapping(
56+
polymorphic_type: options[:as],
57+
class_name: options[:class_name] || name.to_s.classify
58+
)
59+
60+
if options[:as] && (polymorphic_type_mapping || integer_type)
4461
poly_type = options.delete(:as)
45-
mapping = PolymorphicIntegerType::Mapping[poly_type]
46-
klass_mapping = (mapping||{}).key self.sti_name
47-
raise "Polymorphic Class Mapping is missing for #{poly_type}" unless klass_mapping
62+
polymorphic_type_mapping ||= PolymorphicIntegerType::Mapping[poly_type]
63+
if polymorphic_type_mapping == nil
64+
raise "Polymorphic type mapping missing for #{poly_type.inspect}"
65+
end
66+
67+
klass_mapping = (polymorphic_type_mapping || {}).key(sti_name)
68+
69+
if klass_mapping == nil
70+
raise "Class not found for #{sti_name.inspect} in polymorphic type mapping: #{polymorphic_type_mapping}"
71+
end
4872

4973
options[:foreign_key] ||= "#{poly_type}_id"
5074
foreign_type = options.delete(:foreign_type) || "#{poly_type}_type"
@@ -59,6 +83,17 @@ def remove_type_and_establish_mapping(name, options, scope)
5983
end
6084
end
6185

86+
def retrieve_polymorphic_type_mapping(polymorphic_type:, class_name:)
87+
return if polymorphic_type.nil?
88+
89+
belongs_to_class = class_name.safe_constantize
90+
method_name = "#{polymorphic_type}_type_mapping"
91+
92+
if belongs_to_class && belongs_to_class.respond_to?(method_name)
93+
belongs_to_class.public_send(method_name)
94+
end
95+
end
96+
6297
def has_many(name, scope = nil, options = {}, &extension)
6398
if scope.kind_of? Hash
6499
options = scope

polymorphic_integer_type.gemspec

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,4 @@ Gem::Specification.new do |spec|
2424
spec.add_development_dependency "activerecord", "4.2.0"
2525
spec.add_development_dependency "mysql2", "0.3.16"
2626
spec.add_development_dependency "byebug"
27-
2827
end

0 commit comments

Comments
 (0)