diff --git a/lib/ransack.rb b/lib/ransack.rb index 6e2e84846..53268ae21 100644 --- a/lib/ransack.rb +++ b/lib/ransack.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext' require 'ransack/configuration' require 'ransack/adapters' +require 'polyamorous' Ransack::Adapters.object_mapper.require_constants diff --git a/lib/polyamorous.rb b/polyamorous/lib/polyamorous.rb similarity index 100% rename from lib/polyamorous.rb rename to polyamorous/lib/polyamorous.rb diff --git a/lib/polyamorous/activerecord_5.0_ruby_2/join_association.rb b/polyamorous/lib/polyamorous/activerecord_5.0_ruby_2/join_association.rb similarity index 100% rename from lib/polyamorous/activerecord_5.0_ruby_2/join_association.rb rename to polyamorous/lib/polyamorous/activerecord_5.0_ruby_2/join_association.rb diff --git a/lib/polyamorous/activerecord_5.0_ruby_2/join_dependency.rb b/polyamorous/lib/polyamorous/activerecord_5.0_ruby_2/join_dependency.rb similarity index 100% rename from lib/polyamorous/activerecord_5.0_ruby_2/join_dependency.rb rename to polyamorous/lib/polyamorous/activerecord_5.0_ruby_2/join_dependency.rb diff --git a/lib/polyamorous/activerecord_5.1_ruby_2/join_association.rb b/polyamorous/lib/polyamorous/activerecord_5.1_ruby_2/join_association.rb similarity index 100% rename from lib/polyamorous/activerecord_5.1_ruby_2/join_association.rb rename to polyamorous/lib/polyamorous/activerecord_5.1_ruby_2/join_association.rb diff --git a/lib/polyamorous/activerecord_5.1_ruby_2/join_dependency.rb b/polyamorous/lib/polyamorous/activerecord_5.1_ruby_2/join_dependency.rb similarity index 100% rename from lib/polyamorous/activerecord_5.1_ruby_2/join_dependency.rb rename to polyamorous/lib/polyamorous/activerecord_5.1_ruby_2/join_dependency.rb diff --git a/lib/polyamorous/activerecord_5.2.0_ruby_2/join_association.rb b/polyamorous/lib/polyamorous/activerecord_5.2.0_ruby_2/join_association.rb similarity index 100% rename from lib/polyamorous/activerecord_5.2.0_ruby_2/join_association.rb rename to polyamorous/lib/polyamorous/activerecord_5.2.0_ruby_2/join_association.rb diff --git a/lib/polyamorous/activerecord_5.2.0_ruby_2/join_dependency.rb b/polyamorous/lib/polyamorous/activerecord_5.2.0_ruby_2/join_dependency.rb similarity index 100% rename from lib/polyamorous/activerecord_5.2.0_ruby_2/join_dependency.rb rename to polyamorous/lib/polyamorous/activerecord_5.2.0_ruby_2/join_dependency.rb diff --git a/lib/polyamorous/activerecord_5.2.1_ruby_2/join_association.rb b/polyamorous/lib/polyamorous/activerecord_5.2.1_ruby_2/join_association.rb similarity index 100% rename from lib/polyamorous/activerecord_5.2.1_ruby_2/join_association.rb rename to polyamorous/lib/polyamorous/activerecord_5.2.1_ruby_2/join_association.rb diff --git a/lib/polyamorous/activerecord_5.2.1_ruby_2/join_dependency.rb b/polyamorous/lib/polyamorous/activerecord_5.2.1_ruby_2/join_dependency.rb similarity index 100% rename from lib/polyamorous/activerecord_5.2.1_ruby_2/join_dependency.rb rename to polyamorous/lib/polyamorous/activerecord_5.2.1_ruby_2/join_dependency.rb diff --git a/lib/polyamorous/join.rb b/polyamorous/lib/polyamorous/join.rb similarity index 100% rename from lib/polyamorous/join.rb rename to polyamorous/lib/polyamorous/join.rb diff --git a/lib/polyamorous/swapping_reflection_class.rb b/polyamorous/lib/polyamorous/swapping_reflection_class.rb similarity index 100% rename from lib/polyamorous/swapping_reflection_class.rb rename to polyamorous/lib/polyamorous/swapping_reflection_class.rb diff --git a/lib/polyamorous/tree_node.rb b/polyamorous/lib/polyamorous/tree_node.rb similarity index 100% rename from lib/polyamorous/tree_node.rb rename to polyamorous/lib/polyamorous/tree_node.rb diff --git a/polyamorous/lib/polyamorous/version.rb b/polyamorous/lib/polyamorous/version.rb new file mode 100644 index 000000000..c480c5bbf --- /dev/null +++ b/polyamorous/lib/polyamorous/version.rb @@ -0,0 +1,3 @@ +module Polyamorous + VERSION = '2.1.1' +end diff --git a/polyamorous/polyamorous.gemspec b/polyamorous/polyamorous.gemspec new file mode 100644 index 000000000..d6318b831 --- /dev/null +++ b/polyamorous/polyamorous.gemspec @@ -0,0 +1,37 @@ +# -*- encoding: utf-8 -*- +$:.push File.expand_path("../lib", __FILE__) +require "polyamorous/version" + +Gem::Specification.new do |s| + s.name = "polyamorous" + s.version = Polyamorous::VERSION + s.authors = ["Ernie Miller", "Ryan Bigg", "Jon Atack", "Xiang Li"] + s.email = ["ernie@erniemiller.org", "radarlistener@gmail.com", "jonnyatack@gmail.com", "bigxiang@gmail.com"] + s.homepage = "https://github.com/activerecord-hackery/ransack/tree/master/polyamorous" + s.license = "MIT" + s.summary = %q{ + Loves/is loved by polymorphic belongs_to associations, Ransack, Squeel, MetaSearch... + } + s.description = %q{ + This is just an extraction from Ransack/Squeel. You probably don't want to use this + directly. It extends ActiveRecord's associations to support polymorphic belongs_to + associations. + } + + s.rubyforge_project = "polyamorous" + + s.add_dependency 'activerecord', '>= 5.0' + s.add_development_dependency 'rspec', '~> 3' + s.add_development_dependency 'machinist', '~> 1.0.6' + s.add_development_dependency 'faker', '~> 1.6.5' + s.add_development_dependency 'sqlite3', '~> 1.3.3' + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + s.require_paths = ["lib"] + + # specify any dependencies here; for example: + # s.add_development_dependency "rspec" + # s.add_runtime_dependency "rest-client" +end diff --git a/polyamorous/spec/blueprints/articles.rb b/polyamorous/spec/blueprints/articles.rb new file mode 100644 index 000000000..42e2f14be --- /dev/null +++ b/polyamorous/spec/blueprints/articles.rb @@ -0,0 +1,5 @@ +Article.blueprint do + person + title + body +end \ No newline at end of file diff --git a/polyamorous/spec/blueprints/comments.rb b/polyamorous/spec/blueprints/comments.rb new file mode 100644 index 000000000..1ecc7eb69 --- /dev/null +++ b/polyamorous/spec/blueprints/comments.rb @@ -0,0 +1,5 @@ +Comment.blueprint do + article + person + body +end \ No newline at end of file diff --git a/polyamorous/spec/blueprints/notes.rb b/polyamorous/spec/blueprints/notes.rb new file mode 100644 index 000000000..f1431145a --- /dev/null +++ b/polyamorous/spec/blueprints/notes.rb @@ -0,0 +1,3 @@ +Note.blueprint do + note +end \ No newline at end of file diff --git a/polyamorous/spec/blueprints/people.rb b/polyamorous/spec/blueprints/people.rb new file mode 100644 index 000000000..b7ba882cb --- /dev/null +++ b/polyamorous/spec/blueprints/people.rb @@ -0,0 +1,4 @@ +Person.blueprint do + name + salary +end \ No newline at end of file diff --git a/polyamorous/spec/blueprints/tags.rb b/polyamorous/spec/blueprints/tags.rb new file mode 100644 index 000000000..f0fb3d869 --- /dev/null +++ b/polyamorous/spec/blueprints/tags.rb @@ -0,0 +1,3 @@ +Tag.blueprint do + name { Sham.tag_name } +end \ No newline at end of file diff --git a/polyamorous/spec/helpers/polyamorous_helper.rb b/polyamorous/spec/helpers/polyamorous_helper.rb new file mode 100644 index 000000000..8dff2699b --- /dev/null +++ b/polyamorous/spec/helpers/polyamorous_helper.rb @@ -0,0 +1,26 @@ +module PolyamorousHelper + if ActiveRecord::VERSION::STRING >= "4.1" + def new_join_association(reflection, children, klass) + Polyamorous::JoinAssociation.new reflection, children, klass + end + else + def new_join_association(reflection, join_dependency, parent, klass) + Polyamorous::JoinAssociation.new reflection, join_dependency, parent, klass + end + end + + if ActiveRecord::VERSION::STRING >= "5.2" + def new_join_dependency(klass, associations = {}) + alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(klass.connection, klass.table_name, []) + Polyamorous::JoinDependency.new klass, klass.arel_table, associations, alias_tracker + end + else + def new_join_dependency(klass, associations = {}) + Polyamorous::JoinDependency.new klass, associations, [] + end + end + + def new_join(name, type = Polyamorous::InnerJoin, klass = nil) + Polyamorous::Join.new name, type, klass + end +end diff --git a/polyamorous/spec/polyamorous/join_association_spec.rb b/polyamorous/spec/polyamorous/join_association_spec.rb new file mode 100644 index 000000000..bf843e360 --- /dev/null +++ b/polyamorous/spec/polyamorous/join_association_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +module Polyamorous + describe JoinAssociation do + + join_base, join_association_args, polymorphic = [:join_root, 'parent.children', 'reflection.options[:polymorphic]'] + let(:join_dependency) { new_join_dependency Note, {} } + let(:reflection) { Note.reflect_on_association(:notable) } + let(:parent) { join_dependency.send(join_base) } + let(:join_association) { + eval("new_join_association(reflection, #{join_association_args}, Article)") + } + + it 'leaves the orginal reflection intact for thread safety' do + reflection.instance_variable_set(:@klass, Article) + join_association + .swapping_reflection_klass(reflection, Person) do |new_reflection| + expect(new_reflection.options).not_to equal reflection.options + expect(new_reflection.options).not_to have_key(:polymorphic) + expect(new_reflection.klass).to eq(Person) + expect(reflection.klass).to eq(Article) + end + end + + it 'sets the polymorphic option to true after initializing' do + expect(join_association.instance_eval(polymorphic)).to be true + end + end +end diff --git a/polyamorous/spec/polyamorous/join_dependency_spec.rb b/polyamorous/spec/polyamorous/join_dependency_spec.rb new file mode 100644 index 000000000..dd64dc999 --- /dev/null +++ b/polyamorous/spec/polyamorous/join_dependency_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +module Polyamorous + describe JoinDependency do + + method, join_associations, join_base = + if ActiveRecord::VERSION::STRING >= '4.1' + [:instance_eval, 'join_root.drop(1)', :join_root] + else + [:send, 'join_associations', :join_base] + end + + context 'with symbol joins' do + subject { new_join_dependency Person, articles: :comments } + + specify { expect(subject.send(method, join_associations).size) + .to eq(2) } + specify { expect(subject.send(method, join_associations).map(&:join_type)) + .to be_all { Polyamorous::InnerJoin } } + end + + context 'with has_many :through association' do + subject { new_join_dependency Person, :authored_article_comments } + + specify { expect(subject.send(method, join_associations).size) + .to eq 1 } + specify { expect(subject.send(method, join_associations).first.table_name) + .to eq 'comments' } + end + + context 'with outer join' do + subject { new_join_dependency Person, new_join(:articles, :outer) } + + specify { expect(subject.send(method, join_associations).size) + .to eq 1 } + specify { expect(subject.send(method, join_associations).first.join_type) + .to eq Polyamorous::OuterJoin } + end + + context 'with nested outer joins' do + subject { new_join_dependency Person, + new_join(:articles, :outer) => new_join(:comments, :outer) } + + specify { expect(subject.send(method, join_associations).size) + .to eq 2 } + specify { expect(subject.send(method, join_associations).map(&:join_type)) + .to eq [Polyamorous::OuterJoin, Polyamorous::OuterJoin] } + specify { expect(subject.send(method, join_associations).map(&:join_type)) + .to be_all { Polyamorous::OuterJoin } } + end + + context 'with polymorphic belongs_to join' do + subject { new_join_dependency Note, new_join(:notable, :inner, Person) } + + specify { expect(subject.send(method, join_associations).size) + .to eq 1 } + specify { expect(subject.send(method, join_associations).first.join_type) + .to eq Polyamorous::InnerJoin } + specify { expect(subject.send(method, join_associations).first.table_name) + .to eq 'people' } + end + + context 'with polymorphic belongs_to join and nested symbol join' do + subject { new_join_dependency Note, + new_join(:notable, :inner, Person) => :comments } + + specify { expect(subject.send(method, join_associations).size) + .to eq 2 } + specify { expect(subject.send(method, join_associations).map(&:join_type)) + .to be_all { Polyamorous::InnerJoin } } + specify { expect(subject.send(method, join_associations).first.table_name) + .to eq 'people' } + specify { expect(subject.send(method, join_associations)[1].table_name) + .to eq 'comments' } + end + + context '#left_outer_join in Rails 5 overrides join type specified', + if: ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR < 2 do + + let(:join_type_class) do + new_join_dependency( + Person, + new_join(:articles) + ).join_constraints( + [], + Arel::Nodes::OuterJoin + ).first.joins.map(&:class) + end + + specify { expect(join_type_class).to eq [Arel::Nodes::OuterJoin] } + end + end +end diff --git a/polyamorous/spec/polyamorous/join_spec.rb b/polyamorous/spec/polyamorous/join_spec.rb new file mode 100644 index 000000000..5e2bfc3ec --- /dev/null +++ b/polyamorous/spec/polyamorous/join_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +module Polyamorous + describe Join do + it 'is a tree node' do + join = new_join(:articles, :outer) + expect(join).to be_kind_of(TreeNode) + end + + it 'can be added to a tree' do + join = new_join(:articles, :outer) + + tree_hash = {} + join.add_to_tree(tree_hash) + + expect(tree_hash[join]).to be {} + end + end +end diff --git a/polyamorous/spec/spec_helper.rb b/polyamorous/spec/spec_helper.rb new file mode 100644 index 000000000..21450e3ba --- /dev/null +++ b/polyamorous/spec/spec_helper.rb @@ -0,0 +1,43 @@ +require 'machinist/active_record' +require 'sham' +require 'faker' +require 'polyamorous' + +Time.zone = 'Eastern Time (US & Canada)' + +Dir[File.expand_path('../{helpers,support,blueprints}/**/*.rb', __FILE__)] +.each do |f| + require f +end + +Sham.define do + name { Faker::Name.name } + title { Faker::Lorem.sentence } + body { Faker::Lorem.paragraph } + salary { |index| 30000 + (index * 1000) } + tag_name { Faker::Lorem.words(3).join(' ') } + note { Faker::Lorem.words(7).join(' ') } +end + +RSpec.configure do |config| + config.before(:suite) do + message = "Running Polyamorous specs with #{ + ActiveRecord::Base.connection.adapter_name + }, Active Record #{::ActiveRecord::VERSION::STRING}, Arel #{Arel::VERSION + } and Ruby #{RUBY_VERSION}" + line = '=' * message.length + puts line, message, line + Schema.create + end + config.before(:all) { Sham.reset(:before_all) } + config.before(:each) { Sham.reset(:before_each) } + + config.include PolyamorousHelper +end + +RSpec::Matchers.define :be_like do |expected| + match do |actual| + actual.gsub(/^\s+|\s+$/, '').gsub(/\s+/, ' ').strip == + expected.gsub(/^\s+|\s+$/, '').gsub(/\s+/, ' ').strip + end +end diff --git a/polyamorous/spec/support/schema.rb b/polyamorous/spec/support/schema.rb new file mode 100644 index 000000000..96994e2d4 --- /dev/null +++ b/polyamorous/spec/support/schema.rb @@ -0,0 +1,98 @@ +require 'active_record' + +ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') + +class Person < ActiveRecord::Base + belongs_to :parent, class_name: 'Person', foreign_key: :parent_id + has_many :children, class_name: 'Person', foreign_key: :parent_id + has_many :articles + has_many :comments + has_many :authored_article_comments, through: :articles, + foreign_key: :person_id, source: :comments + has_many :notes, as: :notable +end + +class Article < ActiveRecord::Base + belongs_to :person + has_many :comments + has_and_belongs_to_many :tags + has_many :notes, as: :notable +end + +class Comment < ActiveRecord::Base + belongs_to :article + belongs_to :person +end + +class Tag < ActiveRecord::Base + has_and_belongs_to_many :articles +end + +class Note < ActiveRecord::Base + belongs_to :notable, polymorphic: true +end + +module Schema + def self.create + ActiveRecord::Migration.verbose = false + + ActiveRecord::Schema.define do + create_table :people, force: true do |t| + t.integer :parent_id + t.string :name + t.integer :salary + t.boolean :awesome, default: false + t.timestamps null: false + end + + create_table :articles, force: true do |t| + t.integer :person_id + t.string :title + t.text :body + end + + create_table :comments, force: true do |t| + t.integer :article_id + t.integer :person_id + t.text :body + end + + create_table :tags, force: true do |t| + t.string :name + end + + create_table :articles_tags, force: true, id: false do |t| + t.integer :article_id + t.integer :tag_id + end + + create_table :notes, force: true do |t| + t.integer :notable_id + t.string :notable_type + t.string :note + end + end + + 10.times do + person = Person.make + Note.make(notable: person) + 3.times do + article = Article.make(person: person) + 3.times do + article.tags = [Tag.make, Tag.make, Tag.make] + end + Note.make(notable: article) + 10.times do + Comment.make(article: article) + end + end + 2.times do + Comment.make(person: person) + end + end + + Comment.make( + body: 'First post!', article: Article.make(title: 'Hello, world!') + ) + end +end diff --git a/ransack.gemspec b/ransack.gemspec index b65668e1b..9d2e4d609 100644 --- a/ransack.gemspec +++ b/ransack.gemspec @@ -20,6 +20,7 @@ Gem::Specification.new do |s| s.add_dependency 'activerecord', '>= 5.0' s.add_dependency 'activesupport', '>= 5.0' s.add_dependency 'i18n' + s.add_dependency 'polyamorous', Ransack::VERSION.to_s s.add_development_dependency 'rspec', '~> 3' s.add_development_dependency 'machinist', '~> 1.0.6' s.add_development_dependency 'faker', '~> 0.9.5'