polymorpheus 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []