diff --git a/.gitignore b/.gitignore index 180b2bb4..505ae2b9 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,8 @@ Gemfile.lock ## PROJECT::SPECIFIC data.sqlite3 +mock.sqlite3 sequel_data.sqlite3 + +## DEBUG +.rspec-local diff --git a/.travis.yml b/.travis.yml index ddd30785..13c7ca28 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ branches: matrix: include: - rvm: 1.8.7 - env: RAILS_VERSION=3.2.0 SEQUEL_VERSION=4.0 + env: RAILS_VERSION=3.2.0 SEQUEL_VERSION=4.0 MONGOID_VERSION=2.4 - rvm: 1.9.3 env: RAILS_VERSION=3.2.0 - rvm: 1.9.3 diff --git a/Gemfile b/Gemfile index fe673ad2..69820130 100644 --- a/Gemfile +++ b/Gemfile @@ -43,6 +43,8 @@ group :test do sequel_version = ENV['SEQUEL_VERSION'] ? "~> #{ENV['SEQUEL_VERSION']}" : '>= 4.0' gem 'sequel', sequel_version + mongoid_version = ENV['MONGOID_VERSION'] ? "~> #{ENV['MONGOID_VERSION']}" : '>= 3.0' + gem 'mongoid', mongoid_version end group :development do diff --git a/lib/algoliasearch-rails.rb b/lib/algoliasearch-rails.rb index dc3051a3..55bd0ac0 100644 --- a/lib/algoliasearch-rails.rb +++ b/lib/algoliasearch-rails.rb @@ -1,5 +1,6 @@ require 'algoliasearch' +require 'algoliasearch/database_adapter' require 'algoliasearch/version' require 'algoliasearch/utilities' @@ -120,43 +121,10 @@ def add_attribute(*names, &block) end alias :add_attributes :add_attribute - def is_mongoid?(object) - defined?(::Mongoid::Document) && object.class.include?(::Mongoid::Document) - end - - def is_sequel?(object) - defined?(::Sequel) && object.class < ::Sequel::Model - end - - def is_active_record?(object) - !is_mongoid?(object) && !is_sequel?(object) - end - - def get_default_attributes(object) - if is_mongoid?(object) - # work-around mongoid 2.4's unscoped method, not accepting a block - object.attributes - elsif is_sequel?(object) - object.to_hash - else - object.class.unscoped do - object.attributes - end - end - end - def get_attribute_names(object) get_attributes(object).keys end - def attributes_to_hash(attributes, object) - if attributes - Hash[attributes.map { |name, value| [name.to_s, value.call(object) ] }] - else - {} - end - end - def get_attributes(object) # If a serializer is set, we ignore attributes # everything should be done via the serializer @@ -165,20 +133,14 @@ def get_attributes(object) else if @attributes.nil? || @attributes.length == 0 # no `attribute ...` have been configured, use the default attributes of the model - attributes = get_default_attributes(object) + attributes = DatabaseAdapter.get_default_attributes(object) else # at least 1 `attribute ...` has been configured, therefore use ONLY the one configured - if is_active_record?(object) - object.class.unscoped do - attributes = attributes_to_hash(@attributes, object) - end - else - attributes = attributes_to_hash(@attributes, object) - end + attributes = DatabaseAdapter.get_attributes(@attributes, object) end end - attributes.merge!(attributes_to_hash(@additional_attributes, object)) if @additional_attributes + attributes.merge!(DatabaseAdapter.attributes_to_hash(@additional_attributes, object)) if @additional_attributes if @options[:sanitize] sanitizer = begin @@ -394,20 +356,7 @@ def algoliasearch(options = {}, &block) attr_accessor :highlight_result, :snippet_result - if options[:synchronous] == true - if defined?(::Sequel) && self < Sequel::Model - class_eval do - copy_after_validation = instance_method(:after_validation) - define_method(:after_validation) do |*args| - super(*args) - copy_after_validation.bind(self).call - algolia_mark_synchronous - end - end - else - after_validation :algolia_mark_synchronous if respond_to?(:after_validation) - end - end + DatabaseAdapter.prepare_for_synchronous(self) if options[:synchronous] == true if options[:enqueue] raise ArgumentError.new("Cannot use a enqueue if the `synchronous` option if set") if options[:synchronous] proc = if options[:enqueue] == true @@ -425,68 +374,9 @@ def algoliasearch(options = {}, &block) proc.call(record, remove) unless algolia_without_auto_index_scope end end - unless options[:auto_index] == false - if defined?(::Sequel) && self < Sequel::Model - class_eval do - copy_after_validation = instance_method(:after_validation) - copy_before_save = instance_method(:before_save) - define_method(:after_validation) do |*args| - super(*args) - copy_after_validation.bind(self).call - algolia_mark_must_reindex - end - - define_method(:before_save) do |*args| - copy_before_save.bind(self).call - algolia_mark_for_auto_indexing - super(*args) - end - - sequel_version = Gem::Version.new(Sequel.version) - if sequel_version >= Gem::Version.new('4.0.0') && sequel_version < Gem::Version.new('5.0.0') - copy_after_commit = instance_method(:after_commit) - define_method(:after_commit) do |*args| - super(*args) - copy_after_commit.bind(self).call - algolia_perform_index_tasks - end - else - copy_after_save = instance_method(:after_save) - define_method(:after_save) do |*args| - super(*args) - copy_after_save.bind(self).call - self.db.after_commit do - algolia_perform_index_tasks - end - end - end - end - else - after_validation :algolia_mark_must_reindex if respond_to?(:after_validation) - before_save :algolia_mark_for_auto_indexing if respond_to?(:before_save) - if respond_to?(:after_commit) - after_commit :algolia_perform_index_tasks - elsif respond_to?(:after_save) - after_save :algolia_perform_index_tasks - end - end - end - unless options[:auto_remove] == false - if defined?(::Sequel) && self < Sequel::Model - class_eval do - copy_after_destroy = instance_method(:after_destroy) - - define_method(:after_destroy) do |*args| - copy_after_destroy.bind(self).call - algolia_enqueue_remove_from_index!(algolia_synchronous?) - super(*args) - end - end - else - after_destroy { |searchable| searchable.algolia_enqueue_remove_from_index!(algolia_synchronous?) } if respond_to?(:after_destroy) - end - end + DatabaseAdapter.prepare_for_auto_index(self) unless options[:auto_index] == false + DatabaseAdapter.prepare_for_auto_remove(self) unless options[:auto_remove] == false end def algolia_without_auto_index(&block) @@ -742,15 +632,15 @@ def algolia_must_reindex?(object) algolia_configurations.each do |options, settings| next if options[:slave] || options[:replica] return true if algolia_object_id_changed?(object, options) - settings.get_attribute_names(object).each do |k| - changed_method = attribute_changed_method(k) + settings.get_attribute_names(object).each do |attribute_name| + changed_method = DatabaseAdapter.attribute_changed_method(object, attribute_name) return true if !object.respond_to?(changed_method) || object.send(changed_method) end [options[:if], options[:unless]].each do |condition| case condition when nil when String, Symbol - changed_method = attribute_changed_method(condition) + changed_method = DatabaseAdapter.attribute_changed_method(object, condition) return true if !object.respond_to?(changed_method) || object.send(changed_method) else # if the :if, :unless condition is a anything else, @@ -825,7 +715,7 @@ def algolia_object_id_of(o, options = nil) end def algolia_object_id_changed?(o, options = nil) - m = attribute_changed_method(algolia_object_id_method(options)) + m = DatabaseAdapter.attribute_changed_method(o, algolia_object_id_method(options)) o.respond_to?(m) ? o.send(m) : false end @@ -902,31 +792,7 @@ def algolia_indexing_disabled?(options = nil) end def algolia_find_in_batches(batch_size, &block) - if (defined?(::ActiveRecord) && ancestors.include?(::ActiveRecord::Base)) || respond_to?(:find_in_batches) - find_in_batches(:batch_size => batch_size, &block) - elsif defined?(::Sequel) && self < Sequel::Model - dataset.extension(:pagination).each_page(batch_size, &block) - else - # don't worry, mongoid has its own underlying cursor/streaming mechanism - items = [] - all.each do |item| - items << item - if items.length % batch_size == 0 - yield items - items = [] - end - end - yield items unless items.empty? - end - end - - def attribute_changed_method(attr) - if defined?(::ActiveRecord) && ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 1 || - (defined?(::ActiveRecord) && ActiveRecord::VERSION::MAJOR > 5) - "will_save_change_to_#{attr}?" - else - "#{attr}_changed?" - end + DatabaseAdapter.find_in_batches(self, batch_size, &block) end end @@ -979,12 +845,7 @@ def algolia_mark_for_auto_indexing end def algolia_mark_must_reindex - @algolia_must_reindex = - if defined?(::Sequel) && is_a?(Sequel::Model) - new? || self.class.algolia_must_reindex?(self) - else - new_record? || self.class.algolia_must_reindex?(self) - end + @algolia_must_reindex = DatabaseAdapter.mark_must_reindex(self) true end diff --git a/lib/algoliasearch/database_adapter.rb b/lib/algoliasearch/database_adapter.rb new file mode 100644 index 00000000..86c0f9e8 --- /dev/null +++ b/lib/algoliasearch/database_adapter.rb @@ -0,0 +1,156 @@ +module DatabaseAdapter + extend self + + ### Adapter public methods + + # Return a hash of the attributes for the object + # + # @param attributes [Hash] Collection of named attribute Proc's + # @param object [Class Instance] Instance of the ORM Object + def get_attributes(attributes, object) + determine_instance(object) + adapter.get_attributes(attributes, object) + end + + ## Return a hash of the default attributes for the object + # + # @param object [Class Instance] Instance of the ORM Object + def get_default_attributes(object) + determine_instance(object) + adapter.get_default_attributes(object) + end + + ## Mark an object as required for reindexing in the ORM + # + # @param object [Class Instance] Instance of the ORM Object + def mark_must_reindex(object) + determine_instance(object) + adapter.mark_must_reindex(object) + end + + ## Find in batches on the ORM Class + # + # @param klass [Class] The ORM Class + # @param batch_size [Integer] Number of records to fetch per batch + # @param &block [Proc] Block to evaluate in the ORM context + def find_in_batches(klass, batch_size, &block) + determine_class(klass) + adapter.find_in_batches(klass, batch_size, &block) + end + + ## Set the ORM callbacks required for auto indexing + # + # @param klass [Class] The ORM Class + def prepare_for_auto_index(klass) + determine_class(klass) + adapter.prepare_for_auto_index(klass) + end + + ## Set the ORM callbacks required for auto removing objects + # + # @param klass [Class] The ORM Class + def prepare_for_auto_remove(klass) + determine_class(klass) + adapter.prepare_for_auto_remove(klass) + end + + ## Set the ORM callbacks required for synchronous indexing + # + # @param klass [Class] The ORM Class + def prepare_for_synchronous(klass) + determine_class(klass) + adapter.prepare_for_synchronous(klass) + end + + ## Determine the correct changed method to send to the ORM class + # + # This method is not ORM specific so isn't forwared to the + # ORM adapter. We assert the new ActiveRecord 5.1.2 method + # as `will_save_change_to` if this is not present, return + # the old method + def attribute_changed_method(object, attribute_name) + will_save_method = "will_save_change_to_#{attribute_name}?" + did_change_method = "#{attribute_name}_changed?" + + return will_save_method if object.respond_to?(will_save_method) + did_change_method + end + + #### Helper Methods + + # This method is a helper for get_attributes + # + # @param attributes [Hash] Collection of named attribute Proc's + # @param object [Class Instance] Instance of the ORM Object + def attributes_to_hash(attributes, object) + if attributes + Hash[attributes.map { |name, value| [name.to_s, value.call(object) ] }] + else + {} + end + end + + private + + ## Return the database adapter instance (default active_record) + def adapter + return @adapter if @adapter + self.adapter = :active_record + @adapter + end + + ## Set the database adapter + # + # @param adapter [Symbol] A symbol representation of the ORM + def adapter=(adapter) + require "algoliasearch/database_adapter/#{adapter}" + @adapter = DatabaseAdapter.const_get(adapter.to_s.split("_").each(&:capitalize!).join) + end + + ## ORM is evaluated per object. + # + # @param object [Class Instance] Instance of the ORM Object + def determine_instance(object) + self.adapter = :mongoid if is_mongoid?(object) + self.adapter = :sequel if is_sequel?(object) + self.adapter = :active_record if is_active_record?(object) + end + + ## ORM is determined on the class + # + # @param klass [Class] The ORM Class + def determine_class(klass) + self.adapter = :mongoid if is_mongoid_class?(klass) + self.adapter = :sequel if is_sequel_class?(klass) + self.adapter = :active_record if is_active_record_class?(klass) + end + + + #### Database Adapter Class Determination + + def is_mongoid_class?(klass) + defined?(::Mongoid::Document) && klass.include?(::Mongoid::Document) + end + + def is_sequel_class?(klass) + defined?(::Sequel) && klass < ::Sequel::Model + end + + def is_active_record_class?(klass) + (defined?(::ActiveRecord) && klass.ancestors.include?(::ActiveRecord::Base)) || klass.respond_to?(:find_in_batches) + end + + #### Database Adapter Object Determination + + def is_mongoid?(object) + defined?(::Mongoid::Document) && object.class.include?(::Mongoid::Document) + end + + def is_sequel?(object) + defined?(::Sequel) && object.class < ::Sequel::Model + end + + def is_active_record?(object) + !is_mongoid?(object) && !is_sequel?(object) + end +end diff --git a/lib/algoliasearch/database_adapter/active_record.rb b/lib/algoliasearch/database_adapter/active_record.rb new file mode 100644 index 00000000..ece5a4c2 --- /dev/null +++ b/lib/algoliasearch/database_adapter/active_record.rb @@ -0,0 +1,49 @@ +module DatabaseAdapter + module ActiveRecord + extend self + + def get_default_attributes(object) + object.class.unscoped do + object.attributes + end + end + + def get_attributes(attributes, object) + object.class.unscoped do + return DatabaseAdapter.attributes_to_hash(attributes, object) + end + end + + def find_in_batches(klass, batch_size, &block) + klass.find_in_batches(:batch_size => batch_size, &block) + end + + def prepare_for_auto_index(klass) + klass.class_eval do + after_validation :algolia_mark_must_reindex if respond_to?(:after_validation) + before_save :algolia_mark_for_auto_indexing if respond_to?(:before_save) + if respond_to?(:after_commit) + after_commit :algolia_perform_index_tasks + elsif respond_to?(:after_save) + after_save :algolia_perform_index_tasks + end + end + end + + def prepare_for_auto_remove(klass) + klass.class_eval do + after_destroy { |searchable| searchable.algolia_enqueue_remove_from_index!(algolia_synchronous?) } if respond_to?(:after_destroy) + end + end + + def prepare_for_synchronous(klass) + klass.class_eval do + after_validation :algolia_mark_synchronous if respond_to?(:after_validation) + end + end + + def mark_must_reindex(object) + object.new_record? || object.class.algolia_must_reindex?(object) + end + end +end diff --git a/lib/algoliasearch/database_adapter/mongoid.rb b/lib/algoliasearch/database_adapter/mongoid.rb new file mode 100644 index 00000000..f2a484ee --- /dev/null +++ b/lib/algoliasearch/database_adapter/mongoid.rb @@ -0,0 +1,54 @@ +module DatabaseAdapter + module Mongoid + extend self + + # work-around mongoid 2.4's unscoped method, not accepting a block + def get_default_attributes(object) + object.attributes + end + + def get_attributes(attributes, object) + DatabaseAdapter.attributes_to_hash(attributes, object) + end + + def find_in_batches(klass, batch_size, &block) + items = [] + klass.all.each do |item| + items << item + if items.length % batch_size == 0 + yield items + items = [] + end + end + yield items unless items.empty? + end + + def prepare_for_auto_index(klass) + klass.class_eval do + after_validation :algolia_mark_must_reindex if respond_to?(:after_validation) + before_save :algolia_mark_for_auto_indexing if respond_to?(:before_save) + if respond_to?(:after_commit) + after_commit :algolia_perform_index_tasks + elsif respond_to?(:after_save) + after_save :algolia_perform_index_tasks + end + end + end + + def prepare_for_auto_remove(klass) + klass.class_eval do + after_destroy { |searchable| searchable.algolia_enqueue_remove_from_index!(algolia_synchronous?) } if respond_to?(:after_destroy) + end + end + + def prepare_for_synchronous(klass) + klass.class_eval do + after_validation :algolia_mark_synchronous if respond_to?(:after_validation) + end + end + + def mark_must_reindex(object) + object.new_record? || object.class.algolia_must_reindex?(object) + end + end +end diff --git a/lib/algoliasearch/database_adapter/sequel.rb b/lib/algoliasearch/database_adapter/sequel.rb new file mode 100644 index 00000000..3ddd3fc4 --- /dev/null +++ b/lib/algoliasearch/database_adapter/sequel.rb @@ -0,0 +1,83 @@ +module DatabaseAdapter + module Sequel + extend self + + def get_default_attributes(object) + object.to_hash + end + + def get_attributes(attributes, object) + DatabaseAdapter.attributes_to_hash(attributes, object) + end + + def find_in_batches(klass, batch_size, &block) + klass.dataset.extension(:pagination).each_page(batch_size, &block) + end + + def prepare_for_auto_index(klass) + klass.class_eval do + copy_after_validation = instance_method(:after_validation) + copy_before_save = instance_method(:before_save) + + define_method(:after_validation) do |*args| + super(*args) + copy_after_validation.bind(self).call + algolia_mark_must_reindex + end + + define_method(:before_save) do |*args| + copy_before_save.bind(self).call + algolia_mark_for_auto_indexing + super(*args) + end + + sequel_version = Gem::Version.new(::Sequel.version) + if sequel_version >= Gem::Version.new('4.0.0') && sequel_version < Gem::Version.new('5.0.0') + copy_after_commit = instance_method(:after_commit) + define_method(:after_commit) do |*args| + super(*args) + copy_after_commit.bind(self).call + algolia_perform_index_tasks + end + else + copy_after_save = instance_method(:after_save) + define_method(:after_save) do |*args| + super(*args) + copy_after_save.bind(self).call + self.db.after_commit do + algolia_perform_index_tasks + end + end + end + end + end + + def prepare_for_auto_remove(klass) + klass.class_eval do + copy_after_destroy = instance_method(:after_destroy) + + define_method(:after_destroy) do |*args| + copy_after_destroy.bind(self).call + algolia_enqueue_remove_from_index!(algolia_synchronous?) + super(*args) + end + end + end + + def prepare_for_synchronous(klass) + klass.class_eval do + copy_after_validation = instance_method(:after_validation) + define_method(:after_validation) do |*args| + super(*args) + copy_after_validation.bind(self).call + algolia_mark_synchronous + end + end + end + + def mark_must_reindex(object) + object.new? || object.class.algolia_must_reindex?(object) + end + + end +end diff --git a/spec/algoliasearch/database_adapter/active_record_spec.rb b/spec/algoliasearch/database_adapter/active_record_spec.rb new file mode 100644 index 00000000..4f807c2b --- /dev/null +++ b/spec/algoliasearch/database_adapter/active_record_spec.rb @@ -0,0 +1,13 @@ +require "algoliasearch/database_adapter" +require "algoliasearch/database_adapter/active_record" + +require "support/database_adapter" + +## Adapters will use methods defined on the ORM +# +# We should not test too deeply these methods, but +# only assert any logical flow on the adapter + +RSpec.describe DatabaseAdapter::ActiveRecord do + it_behaves_like "database_adapter" +end diff --git a/spec/algoliasearch/database_adapter/mongoid_spec.rb b/spec/algoliasearch/database_adapter/mongoid_spec.rb new file mode 100644 index 00000000..d108f290 --- /dev/null +++ b/spec/algoliasearch/database_adapter/mongoid_spec.rb @@ -0,0 +1,13 @@ +require "algoliasearch/database_adapter" +require "algoliasearch/database_adapter/mongoid" + +require "support/database_adapter" + +## Adapters will use methods defined on the ORM +# +# We should not test too deeply these methods, but +# only assert any logical flow on the adapter + +RSpec.describe DatabaseAdapter::Mongoid do + it_behaves_like "database_adapter" +end diff --git a/spec/algoliasearch/database_adapter/sequel_spec.rb b/spec/algoliasearch/database_adapter/sequel_spec.rb new file mode 100644 index 00000000..ad5d0f9e --- /dev/null +++ b/spec/algoliasearch/database_adapter/sequel_spec.rb @@ -0,0 +1,13 @@ +require "algoliasearch/database_adapter" +require "algoliasearch/database_adapter/sequel" + +require "support/database_adapter" + +## Adapters will use methods defined on the ORM +# +# We should not test too deeply these methods, but +# only assert any logical flow on the adapter + +RSpec.describe DatabaseAdapter::Sequel do + it_behaves_like "database_adapter" +end diff --git a/spec/algoliasearch/database_adapter_spec.rb b/spec/algoliasearch/database_adapter_spec.rb new file mode 100644 index 00000000..f01b198d --- /dev/null +++ b/spec/algoliasearch/database_adapter_spec.rb @@ -0,0 +1,121 @@ +require "spec_helper" +connect_to_db("mock") + +require "algoliasearch/database_adapter" + +## Eager load all ORM Adapters +require "algoliasearch/database_adapter/active_record" +require "algoliasearch/database_adapter/mongoid" +require "algoliasearch/database_adapter/sequel" + +## Support files for basic classes +require "support/mocked_orm_classes" + +ADAPTERS = [ + { "name" => :active_record, "adapter" => DatabaseAdapter::ActiveRecord, "mocked_class" => SimpleActiveRecord }, + { "name" => :sequel, "adapter" => DatabaseAdapter::Sequel, "mocked_class" => SimpleSequel }, + { "name" => :mongoid, "adapter" => DatabaseAdapter::Mongoid, "mocked_class" => SimpleMongoid } +] + +RSpec.describe DatabaseAdapter do + + ## Ensure that these specs use the mock DB setup in + ## mocked_orm_classes.rb + describe "public methods", :mocked_db => true do + + ADAPTERS.each do | test_block | + describe "when #{test_block['name']} object or class" do + + describe "methods sending object" do + [:get_default_attributes, :mark_must_reindex].each do |adapter_method| + it "delegates ##{adapter_method} to #{test_block['adapter']}" do + # Arrange + allow(test_block['adapter']).to receive(adapter_method) + # Act + DatabaseAdapter.send(adapter_method, test_block['mocked_class'].new()) + # Assert + expect(test_block['adapter']).to have_received(adapter_method) + end + end + + it "delegates #get_attributes to #{test_block['adapter']}" do + # Arrange + allow(test_block['adapter']).to receive(:get_attributes) + # Act + DatabaseAdapter.get_attributes({}, test_block['mocked_class'].new()) + # Assert + expect(test_block['adapter']).to have_received(:get_attributes) + end + end + + describe "methods sending klass" do + [:prepare_for_auto_index, :prepare_for_auto_remove, :prepare_for_synchronous].each do |adapter_method| + it "delegates ##{adapter_method} to #{test_block['adapter']}" do + # Arrange + allow(test_block['adapter']).to receive(adapter_method) + # Act + DatabaseAdapter.send(adapter_method, test_block['mocked_class']) + # Assert + expect(test_block['adapter']).to have_received(adapter_method) + end + end + + it "delegates #find_in_batches to #{test_block['adapter']}" do + # Arrange + allow(test_block['adapter']).to receive(:find_in_batches) + # Act + DatabaseAdapter.find_in_batches(test_block['mocked_class'], 10) do Proc.new { |x| x*1 } end + # Assert + expect(test_block['adapter']).to have_received(:find_in_batches) + end + end + + end + end + end + + describe "Private Methods" do + describe "#adapter" do + it "returns the adpater if it is already set" do + # Act + described_class.send(:adapter=, :sequel) + # Assert + expect(described_class.send(:adapter)).to eq DatabaseAdapter::Sequel + end + end + + describe "#adapter=" do + it "errors on missing adapters" do + # Assert + expect { described_class.send(:adapter=, :example) }.to raise_error LoadError + end + end + + describe "#determine_instance" do + it "defaults to active record" do + # Arrange + described_class.send(:determine_instance, Class.new()) + # Assert + expect(described_class.instance_variable_get(:@adapter)).to eq DatabaseAdapter::ActiveRecord + end + + it "sets sequel when is_sequel?" do + # Arrange + allow(described_class).to receive(:is_sequel?).and_return(true) + # Act + described_class.send(:determine_instance, Class.new()) + # Assert + expect(described_class.instance_variable_get(:@adapter)).to eq DatabaseAdapter::Sequel + end + + it "sets mongoid when is_mongoid?" do + # Arrange + allow(described_class).to receive(:is_mongoid?).and_return(true) + # Act + described_class.send(:determine_instance, Class.new()) + # Assert + expect(described_class.instance_variable_get(:@adapter)).to eq DatabaseAdapter::Mongoid + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0616685d..9b9b0d75 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -33,6 +33,16 @@ Algolia.client.delete_index!(index['name']) end end + + # Before each example connect to the default db + c.before(:each) do + connect_to_db() + end + + # Before each example marked `mocked_db: true` switch database + c.before(:each, :mocked_db => true) do + connect_to_db("mock") + end end # A unique prefix for your test run in local or CI @@ -49,3 +59,14 @@ def safe_index_list list = list.select { |index| index["name"].include?(SAFE_INDEX_PREFIX) } list.sort_by { |index| index["primary"] || "" } end + +# Perform database switching in specs so that unit specs can be +# seperated from integration specs in the same run +def connect_to_db(name = "data") + ActiveRecord::Base.establish_connection( + 'adapter' => defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3', + 'database' => "#{name}.sqlite3", + 'pool' => 5, + 'timeout' => 5000 + ) +end diff --git a/spec/support/database_adapter.rb b/spec/support/database_adapter.rb new file mode 100644 index 00000000..98616ae9 --- /dev/null +++ b/spec/support/database_adapter.rb @@ -0,0 +1,12 @@ +shared_examples "database_adapter" do + + [ + :get_attributes, :get_default_attributes, :find_in_batches, :prepare_for_auto_index, + :prepare_for_auto_remove, :prepare_for_synchronous, :mark_must_reindex + ].each do |adapter_method| + it "#{described_class} implements #{adapter_method}" do + expect(described_class.method_defined? adapter_method).to eq true + end + end + +end diff --git a/spec/support/mocked_orm_classes.rb b/spec/support/mocked_orm_classes.rb new file mode 100644 index 00000000..19374048 --- /dev/null +++ b/spec/support/mocked_orm_classes.rb @@ -0,0 +1,47 @@ +require "active_record" +require 'sequel' +require "mongoid" + +## Active record basic class + +# Create a seperate database for mocks (mock.sqlite3) + +FileUtils.rm( 'mock.sqlite3' ) rescue nil +ActiveRecord::Base.logger = Logger.new(STDOUT) +ActiveRecord::Base.logger.level = Logger::WARN + +if ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) + ActiveRecord::Base.raise_in_transactional_callbacks = true +end + +ActiveRecord::Schema.define do + create_table :simple_active_records do |t| + + end +end + +class SimpleActiveRecord < ActiveRecord::Base; end + +## Sequel basic class + +SEQUEL_MOCK_DB = Sequel.connect(defined?(JRUBY_VERSION) ? 'jdbc:sqlite:sequel_data.sqlite3' : { 'adapter' => 'sqlite', 'database' => 'sequel_data.sqlite3' }) + +unless SEQUEL_MOCK_DB.table_exists?(:simple_sequels) + SEQUEL_MOCK_DB.create_table(:simple_sequels) do + primary_key :id + String :name + String :author + FalseClass :released + FalseClass :premium + end +end + +class SimpleSequel < Sequel::Model(SEQUEL_MOCK_DB) + plugin :active_model +end + +## Mogoid basic class + +class SimpleMongoid + include ::Mongoid::Document +end