polymorpheus 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ The MIT LICENSE
2
+
3
+ Copyright (c) 2011 Wegowise Inc.
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.
data/README.md ADDED
File without changes
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ require 'rake'
2
+ require 'fileutils'
3
+
4
+ def gemspec_name
5
+ @gemspec_name ||= Dir['*.gemspec'][0]
6
+ end
7
+
8
+ def gemspec
9
+ @gemspec ||= eval(File.read(gemspec_name), binding, gemspec_name)
10
+ end
11
+
12
+ desc "Build the gem"
13
+ task :gem=>:gemspec do
14
+ sh "gem build #{gemspec_name}"
15
+ FileUtils.mkdir_p 'pkg'
16
+ FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", 'pkg'
17
+ end
18
+
19
+ desc "Install the gem locally"
20
+ task :install => :gem do
21
+ sh %{gem install pkg/#{gemspec.name}-#{gemspec.version}}
22
+ end
23
+
24
+ desc "Generate the gemspec"
25
+ task :generate do
26
+ puts gemspec.to_ruby
27
+ end
28
+
29
+ desc "Validate the gemspec"
30
+ task :gemspec do
31
+ gemspec.validate
32
+ end
33
+
34
+ desc 'Run tests'
35
+ task :test do |t|
36
+ sh 'rspec spec'
37
+ end
38
+
39
+ task :default => :test
@@ -0,0 +1,23 @@
1
+ module Polymorpheus
2
+ class Adapter
3
+
4
+ class << self
5
+ @@registered_adapters = {}
6
+
7
+ def register(adapter_name, file_name)
8
+ @@registered_adapters[adapter_name] = file_name
9
+ end
10
+
11
+ def load!
12
+ if file = @@registered_adapters[configured_adapter]
13
+ require file
14
+ end
15
+ end
16
+
17
+ def configured_adapter
18
+ ActiveRecord::Base.connection_pool.spec.config[:adapter]
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,97 @@
1
+ module Polymorpheus
2
+ module Interface
3
+ class PolymorphicError < ::StandardError; end
4
+
5
+ class InvalidTypeError < PolymorphicError
6
+ def initialize(*accepted_classes)
7
+ error = "Invalid type."
8
+ error += " Must be one of {#{accepted_classes.join(', ')}}"
9
+ super(error)
10
+ end
11
+ end
12
+
13
+ class AmbiguousTypeError < PolymorphicError
14
+ def initialize
15
+ super("Ambiguous polymorphic interface or object type")
16
+ end
17
+ end
18
+
19
+ def self.included(base)
20
+ base.extend(ClassMethods)
21
+ end
22
+
23
+ module ClassMethods
24
+
25
+ def belongs_to_polymorphic(*args)
26
+ options = args.extract_options!
27
+ polymorphic_api = options[:as]
28
+ associations = args.collect(&:to_s).collect(&:downcase)
29
+ association_keys = associations.collect{|association| "#{association}_id"}
30
+
31
+ # Set belongs_to assocaitions
32
+ associations.each do |associated_model|
33
+ belongs_to associated_model.to_sym
34
+ end
35
+
36
+ # Class constant defining the keys
37
+ const_set "#{polymorphic_api}_keys".upcase, association_keys
38
+
39
+ # Helper methods and constants
40
+ define_method "#{polymorphic_api}_types" do
41
+ associations
42
+ end
43
+
44
+ define_method "#{polymorphic_api}_active_key" do
45
+ keys = association_keys.select { |key| self.send(key).present? }
46
+ keys.first if keys.length == 1
47
+ end
48
+
49
+ define_method "#{polymorphic_api}_query_condition" do
50
+ fk = self.send("#{polymorphic_api}_active_key")
51
+ { fk.to_s => self.send(fk) } if fk
52
+ end
53
+
54
+ # Getter method
55
+ define_method polymorphic_api do
56
+ if key = self.send("#{polymorphic_api}_active_key")
57
+ self.send key.gsub(/_id$/,'')
58
+ end
59
+ end
60
+
61
+ # Setter method
62
+ define_method "#{polymorphic_api}=" do |polymorphic_obj|
63
+ possible_associations = [ polymorphic_obj.class.name.underscore,
64
+ polymorphic_obj.class.superclass.name.underscore ]
65
+ match = associations & possible_associations
66
+ if match.blank?
67
+ raise Polymorpheus::Interface::PolymorphicError, associations
68
+ elsif match.length > 1
69
+ raise Polymorpheus::Interface::AmbiguousTypeError
70
+ else
71
+ self.send("#{match[0]}_id=", polymorphic_obj.id)
72
+ (associations - match).each do |association_to_reset|
73
+ self.send("#{association_to_reset}_id=", nil)
74
+ end
75
+ end
76
+ end
77
+
78
+ # Private method called as part of validation
79
+ # Validate that there is exactly one associated object
80
+ define_method "polymorphic_#{polymorphic_api}_relationship_is_valid" do
81
+ if !self.send(polymorphic_api)
82
+ self.errors.add(:base,
83
+ "You must specify exactly one of the following: {#{associations.join(', ')}}")
84
+ end
85
+ end
86
+ private "polymorphic_#{polymorphic_api}_relationship_is_valid"
87
+
88
+ end
89
+
90
+ def validates_polymorph(polymorphic_api)
91
+ validate "polymorphic_#{polymorphic_api}_relationship_is_valid"
92
+ end
93
+
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,127 @@
1
+ module Polymorpheus
2
+ module ConnectionAdapters
3
+ module MysqlAdapter
4
+
5
+ INSERT = 'INSERT'
6
+ UPDATE = 'UPDATE'
7
+
8
+ # Suppose I have a table named "pets" with columns dog_id and kitty_id, and I want them to
9
+ # have a polymorphic database constraint such that dog_id references the "id" column of the
10
+ # dogs table, and kitty_id references the "name" column of the "cats" table. then my inputs
11
+ # to this method would be:
12
+ # table: 'pets'
13
+ # columns: { 'dog_id' => 'dogs.id', 'kitty_id' => 'cats.name' }
14
+ #
15
+ # UNIQUENESS CONSTRAINTS:
16
+ #
17
+ # Suppose the pets table also has a 'person_id' column, and we want to impose a uniqueness
18
+ # constraint such that a given cat or dog can only be associated with the person one time
19
+ # We can specify this in the options as follows:
20
+ # options: :unique => 'person_id'
21
+
22
+ def add_polymorphic_constraints(table, columns, options = {})
23
+ poly_drop_triggers(table)
24
+ poly_create_triggers(table, columns.keys)
25
+ options.symbolize_keys!
26
+ index_suffix = options[:index_suffix]
27
+ if options[:unique].present?
28
+ poly_create_indexes(table, columns.keys, Array(options[:unique]), index_suffix)
29
+ end
30
+ columns.each do |(col, reference)|
31
+ ref_table, ref_col = reference.to_s.split('.')
32
+ add_foreign_key table, ref_table, :column => col, :primary_key => (ref_col || 'id')
33
+ end
34
+ end
35
+
36
+ def remove_polymorphic_constraints(table, columns, options = {})
37
+ poly_drop_triggers(table)
38
+ index_suffix = options[:index_suffix]
39
+ columns.each do |(col, reference)|
40
+ ref_table, ref_col = reference.to_s.split('.')
41
+ remove_foreign_key table, ref_table
42
+ end
43
+ if options[:unique].present?
44
+ poly_remove_indexes(table, columns.keys, Array(options[:unique]), index_suffix)
45
+ end
46
+ end
47
+
48
+
49
+ ##########################################################################
50
+ private
51
+
52
+ def poly_trigger_name(table, action)
53
+ "#{table}_unique_polyfk_on_#{action}"
54
+ end
55
+
56
+ def poly_drop_trigger(table, action)
57
+ execute %{DROP TRIGGER IF EXISTS #{poly_trigger_name(table, action)}}
58
+ end
59
+
60
+ def poly_create_trigger(table, action, columns)
61
+ sql = "CREATE TRIGGER #{poly_trigger_name(table, action)} BEFORE #{action} ON #{table}\n" +
62
+ "FOR EACH ROW\n" +
63
+ "BEGIN\n"
64
+ colchecks = columns.collect { |col| "IF(NEW.#{col} IS NULL, 0, 1)" }.
65
+ join(' + ')
66
+ sql += "IF(#{colchecks}) <> 1 THEN\n" +
67
+ "SET NEW = 'Error';\n" +
68
+ "END IF;\n" +
69
+ "END"
70
+
71
+ execute sql
72
+ end
73
+
74
+ def poly_drop_triggers(table)
75
+ poly_drop_trigger(table, 'INSERT')
76
+ poly_drop_trigger(table, 'UPDATE')
77
+ end
78
+
79
+ def poly_create_triggers(table, columns)
80
+ poly_create_trigger(table, 'INSERT', columns)
81
+ poly_create_trigger(table, 'UPDATE', columns)
82
+ end
83
+
84
+ def poly_create_index(table, column, unique_cols, index_suffix)
85
+ unique_cols = unique_cols.collect(&:to_s)
86
+ name = poly_index_name(table, column, unique_cols, index_suffix)
87
+ execute %{
88
+ CREATE UNIQUE INDEX #{name} ON #{table} (#{column},#{unique_cols.join(',')})
89
+ }
90
+ end
91
+
92
+ def poly_remove_index(table, column, unique_cols, index_suffix)
93
+ unique_cols = unique_cols.collect(&:to_s)
94
+ name = poly_index_name(table, column, unique_cols, index_suffix)
95
+ execute %{ DROP INDEX #{name} ON #{table} }
96
+ end
97
+
98
+ def poly_index_name(table, column, unique_cols, index_suffix)
99
+ index_suffix ||= unique_cols.join('_and_')
100
+ "index_#{table}_on_#{column}_and_#{index_suffix}"
101
+ end
102
+
103
+ def poly_create_indexes(table, columns, unique_cols, index_suffix)
104
+ columns.each do |column|
105
+ poly_create_index(table, column, unique_cols, index_suffix)
106
+ end
107
+ end
108
+
109
+ def poly_remove_indexes(table, columns, unique_cols, index_suffix)
110
+ columns.each do |column|
111
+ poly_remove_index(table, column, unique_cols, index_suffix)
112
+ end
113
+ end
114
+
115
+ end
116
+ end
117
+ end
118
+
119
+ [:MysqlAdapter, :Mysql2Adapter].each do |adapter|
120
+ begin
121
+ ActiveRecord::ConnectionAdapters.const_get(adapter).class_eval do
122
+ include Polymorpheus::ConnectionAdapters::MysqlAdapter
123
+ end
124
+ rescue
125
+ end
126
+ end
127
+
@@ -0,0 +1,20 @@
1
+ # Thanks to matthuhiggins/foreigner gem for the template used here
2
+ module Polymorpheus
3
+ class Railtie < Rails::Railtie
4
+
5
+ initializer 'polymorpheus.load_adapter' do
6
+ ActiveSupport.on_load :active_record do
7
+
8
+ ActiveRecord::Base.send :include, Polymorpheus::Interface
9
+
10
+ ActiveRecord::ConnectionAdapters.module_eval do
11
+ include Polymorpheus::ConnectionAdapters::SchemaStatements
12
+ end
13
+
14
+ Polymorpheus::Adapter.load!
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+
@@ -0,0 +1,17 @@
1
+ module Polymorpheus
2
+ module ConnectionAdapters
3
+ module SchemaStatements
4
+ def self.included(base)
5
+ base::AbstractAdapter.class_eval do
6
+ include Polymorpheus::ConnectionAdapters::AbstractAdapter
7
+ end
8
+ end
9
+ end
10
+
11
+ module AbstractAdapter
12
+ def add_polymorphic_constraints(table, columns, options = {})
13
+ end
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,3 @@
1
+ module Polymorpheus
2
+ VERSION = '0.1'
3
+ end
@@ -0,0 +1,12 @@
1
+ module Polymorpheus
2
+ autoload :Adapter, 'polymorpheus/adapter'
3
+ autoload :Interface, 'polymorpheus/interface'
4
+
5
+ module ConnectionAdapters
6
+ autoload :SchemaStatements, 'polymorpheus/schema_statements'
7
+ end
8
+ end
9
+
10
+ Polymorpheus::Adapter.register 'mysql2', 'polymorpheus/mysql_adapter'
11
+
12
+ require 'polymorpheus/railtie' if defined?(Rails)
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'rubygems' unless defined? Gem
3
+ require File.dirname(__FILE__) + "/lib/polymorpheus/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "polymorpheus"
7
+ s.version = Polymorpheus::VERSION
8
+ s.authors = ["Barun Singh"]
9
+ s.email = "bsingh@wegowise.com"
10
+ s.homepage = "http://github.com/wegowise/polymorpheus"
11
+ s.summary = "Provides a database-friendly method for polymorphic relationships"
12
+ s.description = "Provides a database-friendly method for polymorphic relationships"
13
+ s.required_rubygems_version = ">= 1.3.6"
14
+ s.files = Dir.glob(%w[{lib,spec}/**/*.rb [A-Z]*.{txt,rdoc,md} *.gemspec]) + %w{Rakefile}
15
+ s.extra_rdoc_files = ["README.md", "LICENSE.txt"]
16
+ s.license = 'MIT'
17
+ s.add_dependency('foreigner')
18
+ end
@@ -0,0 +1,75 @@
1
+ require 'active_record'
2
+ require 'polymorpheus'
3
+ require 'spec_helper'
4
+
5
+ # this is normally done via a Railtie in non-testing situations
6
+ ActiveRecord::Base.send :include, Polymorpheus::Interface
7
+
8
+ class Shoe < ActiveRecord::Base
9
+ belongs_to_polymorphic :man, :woman, :as => :wearer
10
+ validates_polymorph :wearer
11
+ end
12
+
13
+ class Man < ActiveRecord::Base
14
+ end
15
+
16
+ class Woman < ActiveRecord::Base
17
+ end
18
+
19
+ describe Shoe do
20
+
21
+ let(:shoe) { Shoe.new(attributes) }
22
+ let(:man) { Man.create! }
23
+ let(:woman) { Woman.create! }
24
+
25
+ describe "class level constant" do
26
+ specify { Shoe::WEARER_KEYS.should == ["man_id", "woman_id"] }
27
+ end
28
+
29
+ describe "helper methods" do
30
+ specify { Shoe.new.wearer_types.should == ["man", "woman"] }
31
+ end
32
+
33
+ it "make the dynamically defined validation method private" do
34
+ Shoe.private_instance_methods.
35
+ include?(:polymorphic_wearer_relationship_is_valid).should be_true
36
+ end
37
+
38
+ shared_examples_for "invalid polymorphic relationship" do
39
+ specify { shoe.wearer.should == nil }
40
+ specify { shoe.wearer_active_key.should == nil }
41
+ specify { shoe.wearer_query_condition.should == nil }
42
+ it "validates appropriately" do
43
+ shoe.valid?.should be_false
44
+ shoe.errors[:base].should ==
45
+ ["You must specify exactly one of the following: {man, woman}"]
46
+ end
47
+ end
48
+
49
+ context "when there is no relationship defined" do
50
+ let(:attributes) { {} }
51
+ it_should_behave_like "invalid polymorphic relationship"
52
+ end
53
+
54
+ context "when there are multiple relationships defined" do
55
+ let(:attributes) { { man_id: man.id, woman_id: woman.id } }
56
+ it_should_behave_like "invalid polymorphic relationship"
57
+ end
58
+
59
+ context "when there is exactly one relationship defined" do
60
+ shared_examples_for "valid polymorphic relationship" do
61
+ specify { shoe.wearer.should == man }
62
+ specify { shoe.wearer_active_key.should == 'man_id' }
63
+ specify { shoe.wearer_query_condition.should == { 'man_id' => man.id } }
64
+ end
65
+ context "and we have specified it via the id value" do
66
+ let(:attributes) { { man_id: man.id } }
67
+ it_should_behave_like "valid polymorphic relationship"
68
+ end
69
+ context "and we have specified it via the id value" do
70
+ let(:attributes) { { man: man } }
71
+ it_should_behave_like "valid polymorphic relationship"
72
+ end
73
+ end
74
+
75
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_record'
2
+ require 'spec_helper'
3
+ require 'sql_logger'
4
+ require 'foreigner'
5
+ require 'foreigner/connection_adapters/mysql2_adapter'
6
+ require 'polymorpheus'
7
+
8
+ Polymorpheus::Adapter.load!
9
+
10
+ describe "Polymorpheus" do
11
+ class << ActiveRecord::Base.connection
12
+ include Polymorpheus::SqlLogger
13
+ end
14
+
15
+ let(:connection) { ActiveRecord::Base.connection }
16
+ let(:sql) { connection.sql_statements }
17
+
18
+ describe "add_polymorphic_constraints" do
19
+ before do
20
+ connection.add_polymorphic_constraints 'pets',
21
+ { 'dog_id' => 'dogs.id', 'kitty_id' => 'cats.name' }
22
+ end
23
+
24
+ it "executes the correct sql statements" do
25
+ clean_sql(sql.join("\n")).should == clean_sql(%{
26
+ DROP TRIGGER IF EXISTS pets_unique_polyfk_on_INSERT
27
+ DROP TRIGGER IF EXISTS pets_unique_polyfk_on_UPDATE
28
+ CREATE TRIGGER pets_unique_polyfk_on_INSERT BEFORE INSERT ON pets
29
+ FOR EACH ROW
30
+ BEGIN
31
+ IF(IF(NEW.dog_id IS NULL, 0, 1) + IF(NEW.kitty_id IS NULL, 0, 1)) <> 1 THEN
32
+ SET NEW = 'Error';
33
+ END IF;
34
+ END
35
+ CREATE TRIGGER pets_unique_polyfk_on_UPDATE BEFORE UPDATE ON pets
36
+ FOR EACH ROW
37
+ BEGIN
38
+ IF(IF(NEW.dog_id IS NULL, 0, 1) + IF(NEW.kitty_id IS NULL, 0, 1)) <> 1 THEN
39
+ SET NEW = 'Error';
40
+ END IF;
41
+ END
42
+
43
+ ALTER TABLE `pets` ADD CONSTRAINT `pets_dog_id_fk` FOREIGN KEY (`dog_id`) REFERENCES `dogs`(id)
44
+ ALTER TABLE `pets` ADD CONSTRAINT `pets_kitty_id_fk` FOREIGN KEY (`kitty_id`) REFERENCES `cats`(name)
45
+ })
46
+ end
47
+
48
+ end
49
+
50
+ def clean_sql(sql_string)
51
+ sql_string.gsub(/^\n\s*/,'').gsub(/\s*\n\s*$/,'').gsub(/\n\s*/,"\n")
52
+ end
53
+
54
+ end
@@ -0,0 +1,21 @@
1
+ ActiveRecord::Base.establish_connection({
2
+ adapter: 'mysql2',
3
+ username: 'root',
4
+ password: '',
5
+ host: 'localhost',
6
+ database: 'polymorphicTest'
7
+ })
8
+
9
+ ActiveRecord::Base.connection.tables.each do |table|
10
+ ActiveRecord::Base.connection.drop_table table
11
+ end
12
+
13
+ ActiveRecord::Schema.define do
14
+ create_table :shoes do |t|
15
+ t.integer :man_id
16
+ t.integer :woman_id
17
+ end
18
+ create_table :men
19
+ create_table :women
20
+ end
21
+
@@ -0,0 +1,17 @@
1
+ module Polymorpheus
2
+ module SqlLogger
3
+
4
+ def sql_statements
5
+ @sql_statements ||= []
6
+ end
7
+
8
+ private
9
+
10
+ def execute(sql, name = nil)
11
+ sql_statements << sql
12
+ sql
13
+ end
14
+
15
+ end
16
+ end
17
+
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: polymorpheus
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Barun Singh
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-13 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: foreigner
16
+ requirement: &70170816060540 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70170816060540
25
+ description: Provides a database-friendly method for polymorphic relationships
26
+ email: bsingh@wegowise.com
27
+ executables: []
28
+ extensions: []
29
+ extra_rdoc_files:
30
+ - README.md
31
+ - LICENSE.txt
32
+ files:
33
+ - lib/polymorpheus/adapter.rb
34
+ - lib/polymorpheus/interface.rb
35
+ - lib/polymorpheus/mysql_adapter.rb
36
+ - lib/polymorpheus/railtie.rb
37
+ - lib/polymorpheus/schema_statements.rb
38
+ - lib/polymorpheus/version.rb
39
+ - lib/polymorpheus.rb
40
+ - spec/interface_spec.rb
41
+ - spec/mysql2_adapter_spec.rb
42
+ - spec/spec_helper.rb
43
+ - spec/sql_logger.rb
44
+ - LICENSE.txt
45
+ - README.md
46
+ - polymorpheus.gemspec
47
+ - Rakefile
48
+ homepage: http://github.com/wegowise/polymorpheus
49
+ licenses:
50
+ - MIT
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: 1.3.6
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 1.8.6
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Provides a database-friendly method for polymorphic relationships
73
+ test_files: []