diff --git a/Gemfile b/Gemfile index 550fc36..d164d51 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ source 'https://rubygems.org' gemspec -gem "actionpack", "~> 4.0" +#gem "actionpack", "~> 4.0" group :test do gem "rspec", "~> 3.0.0.beta1" diff --git a/lib/my_mongoid.rb b/lib/my_mongoid.rb index a44ed83..653b92e 100644 --- a/lib/my_mongoid.rb +++ b/lib/my_mongoid.rb @@ -2,6 +2,7 @@ require "my_mongoid/configuration" require "my_mongoid/session" require "my_mongoid/errors" +require "my_mongoid/my_callbacks" require 'active_support/concern' module MyMongoid @@ -11,7 +12,6 @@ def self.models end def self.register_model(klass) - # why self todo @models << klass unless models.include?(klass) end diff --git a/lib/my_mongoid/attributes.rb b/lib/my_mongoid/attributes.rb index ab98367..ccdef07 100644 --- a/lib/my_mongoid/attributes.rb +++ b/lib/my_mongoid/attributes.rb @@ -5,10 +5,10 @@ module Attributes attr_accessor :attributes def process_attributes(attr) + # adding new key in interation attr.each do |key, val| key_s = key.to_s raise UnknownAttributeError unless self.class.fields[key_s] - # todo 如何调用类方法 send("#{key_s}=", val) end end diff --git a/lib/my_mongoid/callbacks.rb b/lib/my_mongoid/callbacks.rb new file mode 100644 index 0000000..aaaf129 --- /dev/null +++ b/lib/my_mongoid/callbacks.rb @@ -0,0 +1,11 @@ +module MyMongoid + module Callbacks + extend ActiveSupport::Concern + + included do + extend ActiveModel::Callbacks + define_model_callbacks :delete, :save, :create, :update + define_model_callbacks :find, :initialize, :only => :after + end + end +end diff --git a/lib/my_mongoid/document.rb b/lib/my_mongoid/document.rb index 8fb00d0..1ec058e 100644 --- a/lib/my_mongoid/document.rb +++ b/lib/my_mongoid/document.rb @@ -1,7 +1,9 @@ require "my_mongoid/fields" require "my_mongoid/session" require "my_mongoid/attributes" +require "my_mongoid/callbacks" require 'active_support/concern' +require 'active_model' module MyMongoid @@ -9,6 +11,7 @@ module Document extend ActiveSupport::Concern include Fields include Attributes + include Callbacks included do MyMongoid.register_model(self) @@ -44,15 +47,17 @@ def to_document end def save - return true unless self.changed? - if @is_new - self.class.collection.insert(self.to_document) - @is_new = false - else - self.class.collection.find({"_id" => self._id}).update(self.atomic_updates) + run_callbacks(:save) do + return true unless self.changed? + if @is_new + self.class.collection.insert(self.to_document) + @is_new = false + else + self.class.collection.find({"_id" => self._id}).update(self.atomic_updates) + end + @changed_attributes = {} + true end - @changed_attributes = {} - true end def deleted? @@ -77,8 +82,6 @@ def initialize(attributes) raise ArgumentError, 'It is not a hash' unless attributes.is_a?(Hash) @is_new = true @attributes ||= {} - # todo @attributes = attributes 会导致错误 - # adding new key in interation unless attributes.key?('id') or attributes.key?('_id') self._id = BSON::ObjectId.new diff --git a/lib/my_mongoid/my_callbacks.rb b/lib/my_mongoid/my_callbacks.rb new file mode 100644 index 0000000..3423c1d --- /dev/null +++ b/lib/my_mongoid/my_callbacks.rb @@ -0,0 +1,87 @@ +require 'active_support/core_ext' + +module MyMongoid + module MyCallbacks + + def self.included(base) + base.extend ClassMethods + end + + # TODO: I know it could be done by ActiveSupport::Concern. But later? + module ClassMethods + def define_callbacks(name) + class_attribute "_#{name}_callbacks" + send("_#{name}_callbacks=", CallbackChain.new) + end + + def set_callback(name, kind, filter) + send("_#{name}_callbacks").append(Callback.new(filter, kind)) + end + + end + + def run_callbacks(name) + self.class.send("_#{name}_callbacks").invoke(self) do + yield + end + end + + # fancy Array, store all callbacks for one spec + class CallbackChain + attr_accessor :chain, :before_callbacks, :around_callbacks, :after_callbacks + + def initialize + @chain = [] + @before_callbacks = @around_callbacks = @after_callbacks = [] + end + + def empty? + @chain.empty? + end + + def append(callback) + @chain.push callback + end + + def invoke(target, &block) + _invoke(0, target, &block) + end + + protected + + def _invoke(i, target, &block) + if i >= @chain.length + block.call + else + # TODO: dirty? + @chain[i].invoke(target) if @chain[i].kind == :before + if @chain[i].kind == :around + @chain[i].invoke(target) do + _invoke(i+1, target, &block) + end + else + _invoke(i+1, target, &block) + end + @chain[i].invoke(target) if @chain[i].kind == :after + end + end + end + + class Callback + attr_accessor :kind, :filter + + # @param [Symbol] filter The callback method name. + # @param [Symbol] kind The kind of callback + def initialize(filter, kind) + @filter = filter + @kind = kind + end + + def invoke(target, &block) + # TODO: String, Proc & Object Supporting + target.send(filter, &block) + end + end + + end +end diff --git a/my_mongoid.gemspec b/my_mongoid.gemspec index 5358d2c..eac9754 100644 --- a/my_mongoid.gemspec +++ b/my_mongoid.gemspec @@ -21,4 +21,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler", "~> 1.3" spec.add_development_dependency "rake" spec.add_dependency "moped", ["~> 2.0.beta6"] + spec.add_dependency("activesupport", ["~> 4.0.3"]) + spec.add_dependency("activemodel", ["~> 4.0.3"]) end diff --git a/spec/my_mongoid/callbacks-02_spec.rb b/spec/my_mongoid/callbacks-02_spec.rb new file mode 100644 index 0000000..0010ce6 --- /dev/null +++ b/spec/my_mongoid/callbacks-02_spec.rb @@ -0,0 +1,122 @@ +require "spec_helper" + +describe MyMongoid::MyCallbacks do + let(:base) { + Class.new do + include MyMongoid::MyCallbacks + + define_callbacks :save + + def before_1 + end + + def before_2 + end + + def save + run_callbacks(:save) { + _save + } + end + + def _save + end + + def to_s + "#" + end + end + } + + + + describe "run before callbacks recursively" do + let(:klass) { + Class.new(base) { + set_callback :save, :before, :before_1 + set_callback :save, :before, :before_2 + } + } + + let(:target) { + klass.new + } + + after { + target.save + } + + it "should recursively call _invoke" do + expect(target._save_callbacks).to receive(:_invoke).and_call_original.exactly(3).times + end + + it "should call the before methods in order" do + expect(target).to receive(:before_1).ordered + expect(target).to receive(:before_2).ordered + expect(target).to receive(:_save).ordered + end + end + + describe "run after callbacks recursively" do + let(:klass) { + Class.new(base) { + set_callback :save, :after, :after_1 + set_callback :save, :after, :after_2 + } + } + + let(:target) { + klass.new + } + + after { + target.save + } + + it "should call the after methods in order" do + expect(target).to receive(:_save).ordered + expect(target).to receive(:after_2).ordered + expect(target).to receive(:after_1).ordered + end + + end + + describe "run around callbacks recursively" do + let(:klass) { + Class.new(base) { + set_callback :save, :around, :around_1 + set_callback :save, :around, :around_2 + + def around_1 + around_1_top + yield + around_1_bottom + end + + def around_2 + around_2_top + yield + around_2_bottom + end + } + } + + let(:target) { + klass.new + } + + after { + target.save + } + + it "should call the around methods in order" do + expect(target).to receive(:around_1).and_call_original.ordered + expect(target).to receive(:around_1_top).ordered + expect(target).to receive(:around_2).and_call_original.ordered + expect(target).to receive(:around_2_top).ordered + expect(target).to receive(:_save).ordered + expect(target).to receive(:around_2_bottom).ordered + expect(target).to receive(:around_1_bottom).ordered + end + end +end diff --git a/spec/my_mongoid/callbacks_spec.rb b/spec/my_mongoid/callbacks_spec.rb new file mode 100644 index 0000000..2510281 --- /dev/null +++ b/spec/my_mongoid/callbacks_spec.rb @@ -0,0 +1,163 @@ +require "spec_helper" + +describe MyMongoid::MyCallbacks do + let(:base) { + Class.new do + include MyMongoid::MyCallbacks + end + } + + describe ".define_callbacks" do + let(:klass) { + Class.new(base) do + define_callbacks :save + end + } + + it "should declare the class attribute \#{name}_callbacks" do + expect(klass).to respond_to("_save_callbacks") + expect(klass).to respond_to("_save_callbacks=") + end + + it "should initially return an instance of CallbackChain" do + expect(klass._save_callbacks).to be_a(MyMongoid::MyCallbacks::CallbackChain) + end + end + + describe "MyMongoid::MyCallbacks::Callback" do + let(:cb) { + MyMongoid::MyCallbacks::Callback.new(:before_save,:before) + } + + let(:target) { + # wtf? + double() + } + + it "should have the #kind attr_reader" do + expect(cb.kind).to eq(:before) + end + + it "should have the #filter attr_reader" do + expect(cb.filter).to eq(:before_save) + end + + it "should call the target object's method when #invoke is called" do + expect(target).to receive(:before_save) + cb.invoke(target) + end + + # FOR BONUS :) + # Reference: Callback#make_lambda(filter) + + # String: some content to evaluate + # Object: An object with before_save method on it to call + # Proc: A proc to call with the object + context "extend #invoke to support String, Proc and Object as filter" do + let(:cb) { + MyMongoid::MyCallbacks::Callback.new("before_save", :before) + } + + it "should support String" do + expect(target).to receive(:before_save) + cb.invoke(target) + end + + it "should support Proc" do + end + end + + end + + describe "MyMongoid::MyCallbacks::CallbackChain" do + let(:cbchain) { + MyMongoid::MyCallbacks::CallbackChain.new + } + + let(:cb1) { + double() + } + + let(:cb2) { + double() + } + + it "should initially be empty" do + expect(cbchain).to be_empty + end + + it "should initially set @chain to be the empty array" do + expect(cbchain.chain).to eq([]) + end + + it "should be able to append callbacks to the chain" do + cbchain.append(cb1) + cbchain.append(cb2) + expect(cbchain.chain).to eq([cb1,cb2]) + end + + describe "#invoke" do + let(:target) { + double() + } + + before { + cbchain.append(cb1) + cbchain.append(cb2) + } + + after { + cbchain.invoke(target) { + target.main_method + } + } + + it "should call the callbacks in order, then call the block" do + expect(cb1).to receive(:invoke).with(target).ordered + expect(cb2).to receive(:invoke).with(target).ordered + expect(target).to receive(:main_method) + end + end + end + + describe ".set_callback" do + let(:klass) { + Class.new(base) do + define_callbacks :save + set_callback :save, :before, :before_save + end + } + + let(:callback) { + klass._save_callbacks.chain.first + } + + it "should append a callback to the named callback chain" do + expect(callback).to be_a(MyMongoid::MyCallbacks::Callback) + expect(callback.kind).to eq(:before) + expect(callback.filter).to eq(:before_save) + end + end + + describe "#run_callbacks" do + let(:klass) { + Class.new(base) do + define_callbacks :save + set_callback :save, :before, :before_save + end + } + + let(:object) { + klass.new + } + + it "should invoke the callback chain" do + expect(object).to receive(:before_save).ordered + expect(object).to receive(:main_method).ordered + object.run_callbacks(:save) do + object.main_method + end + end + end + +end diff --git a/spec/my_mongoid/document_spec.rb b/spec/my_mongoid/document_spec.rb index 77ded68..fe1adb6 100644 --- a/spec/my_mongoid/document_spec.rb +++ b/spec/my_mongoid/document_spec.rb @@ -1,7 +1,5 @@ require "spec_helper" -# todo - class Event include MyMongoid::Document field :public diff --git a/spec/my_mongoid/lifecycle_spec.rb b/spec/my_mongoid/lifecycle_spec.rb new file mode 100644 index 0000000..98e3181 --- /dev/null +++ b/spec/my_mongoid/lifecycle_spec.rb @@ -0,0 +1,213 @@ +require 'spec_helper' + +class ATM + include MyMongoid::Document + + field :a +end + +def config_db + MyMongoid.configure do |config| + config.host = "127.0.0.1:27017" + config.database = "my_mongoid_test" + end +end + +describe "Should define lifecycle callbacks" do + describe "before, around, after hooks" do + + before(:all) do + class ATM + def do_something + end + + def do_something_around + # something before + yield + # something after + end + end + end + it "should declare before hook for delete" do + expect { + ATM.send(:before_delete, :do_something) + }.not_to raise_error + end + + it "should declare around hook for delete" do + expect { + ATM.send(:around_delete, :do_something_around) + }.not_to raise_error + end + + it "should declare after hook for delete" do + expect { + ATM.send(:after_delete, :do_something) + }.not_to raise_error + end + + it "should declare before hook for save" do + expect { + ATM.send(:before_save, :do_something) + }.not_to raise_error + end + + it "should declare around hook for save" do + expect { + ATM.send(:around_save, :do_something_around) + }.not_to raise_error + end + + it "should declare after hook for save" do + expect { + ATM.send(:after_save, :do_something) + }.not_to raise_error + end + + it "should declare before hook for create" do + expect { + ATM.send(:before_create, :do_something) + }.not_to raise_error + end + + it "should declare around hook for create" do + expect { + ATM.send(:around_create, :do_something_around) + }.not_to raise_error + end + + it "should declare after hook for create" do + expect { + ATM.send(:after_create, :do_something) + }.not_to raise_error + end + + it "should declare before hook for update" do + expect { + ATM.send(:before_update, :do_something) + }.not_to raise_error + end + + it "should declare around hook for update" do + expect { + ATM.send(:around_update, :do_something_around) + }.not_to raise_error + end + + it "should declare after hook for update" do + expect { + ATM.send(:after_update, :do_something) + }.not_to raise_error + end + end + + describe "only after hooks" do + + before(:all) do + class ATM + def do_something + end + + def do_something_around + # something before + yield + # something after + end + end + end + + it "should not declare before hook for find" do + expect { + ATM.send(:before_find, :do_something) + }.to raise_error + end + + it "should not declare around for find" do + expect { + ATM.send(:around_find, :do_something_around) + }.to raise_error + end + + it "should declare after hook for find" do + expect { + ATM.send(:after_find, :do_something) + }.not_to raise_error + end + + it "should not declare before hook for initialize" do + expect { + ATM.send(:before_initialize, :do_something) + }.to raise_error + end + + it "should not declare around for initialize" do + expect { + ATM.send(:around_initialize, :do_something_around) + }.to raise_error + end + + it "should declare after hook for initialize" do + expect { + ATM.send(:after_initialize, :do_something) + }.not_to raise_error + end + + end + + describe "create callbacks" do + + before(:all) do + config_db + class ATM + def gotcha + end + + def got_again + end + end + end + + let(:atm) { + ATM.new({ :a => "omg" }) + } + + it "should run callbacks when saving a new record" do + ATM.send(:before_save, :gotcha) + expect(atm).to receive(:gotcha) + atm.save + end + + it "should run callbacks wehn creating a new record" do + ATM.send(:before_save, :got_again) + expect_any_instance_of(ATM).to receive(:got_again) + ATM.create({ :a => "yeah" }) + end + end + + describe "run save callbacks" do + before(:all) do + config_db + class ATM + def save_callback + end + end + end + + let(:atm) { + ATM.create({ :a => "yes" }) + } + + it "should run callbacks when saving a new record" do + # duplicated with the one above + end + + it "should run callbacks when saving a persisted record" do + ATM.send(:after_save, :save_callback) + expect(atm).not_to be_new_record + expect(atm).to receive(:save_callback) + atm.a = "bye" + atm.save + end + end + +end