diff --git a/activemodel/CHANGES b/activemodel/CHANGES new file mode 100644 index 0000000000..a9f9c27507 --- /dev/null +++ b/activemodel/CHANGES @@ -0,0 +1,12 @@ +Changes from extracting bits to ActiveModel + +* ActiveModel::Observer#add_observer! + + It has a custom hook to define after_find that should really be in a + ActiveRecord::Observer subclass: + + def add_observer!(klass) + klass.add_observer(self) + klass.class_eval 'def after_find() end' unless + klass.respond_to?(:after_find) + end \ No newline at end of file diff --git a/activemodel/README b/activemodel/README new file mode 100644 index 0000000000..c20f732a12 --- /dev/null +++ b/activemodel/README @@ -0,0 +1,21 @@ +Active Model +============== + +Totally experimental library that aims to extract common model mixins from +ActiveRecord for use in ActiveResource (and other similar libraries). +This is in a very rough state (no autotest or spec rake tasks set up yet), +so please excuse the mess. + +Here's what I plan to extract: + * ActiveModel::Observing + * ActiveModel::Callbacks + * ActiveModel::Validations + + # for ActiveResource params and ActiveRecord options + * ActiveModel::Scoping + + # to_json, to_xml, etc + * ActiveModel::Serialization + +I'm trying to keep ActiveRecord compatibility where possible, but I'm +annotating the spots where I'm diverging a bit. \ No newline at end of file diff --git a/activemodel/Rakefile b/activemodel/Rakefile new file mode 100644 index 0000000000..cb9a61773f --- /dev/null +++ b/activemodel/Rakefile @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +$LOAD_PATH << File.join(File.dirname(__FILE__), 'vendor', 'rspec', 'lib') +require 'rake' +require 'spec/rake/spectask' \ No newline at end of file diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb new file mode 100644 index 0000000000..369c7fed33 --- /dev/null +++ b/activemodel/lib/active_model.rb @@ -0,0 +1,17 @@ +$LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..', 'activesupport', 'lib') + +# premature optimization? +require 'active_support/inflector' +require 'active_support/core_ext/string/inflections' +String.send :include, ActiveSupport::CoreExtensions::String::Inflections + +require 'active_model/base' +require 'active_model/observing' +require 'active_model/callbacks' +require 'active_model/validations' + +ActiveModel::Base.class_eval do + include ActiveModel::Observing + include ActiveModel::Callbacks + include ActiveModel::Validations +end \ No newline at end of file diff --git a/activemodel/lib/active_model/base.rb b/activemodel/lib/active_model/base.rb new file mode 100644 index 0000000000..1141156da4 --- /dev/null +++ b/activemodel/lib/active_model/base.rb @@ -0,0 +1,4 @@ +module ActiveModel + class Base + end +end \ No newline at end of file diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb new file mode 100644 index 0000000000..0114fc386b --- /dev/null +++ b/activemodel/lib/active_model/callbacks.rb @@ -0,0 +1,5 @@ +module ActiveModel + module Callbacks + + end +end \ No newline at end of file diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb new file mode 100644 index 0000000000..db758f5185 --- /dev/null +++ b/activemodel/lib/active_model/observing.rb @@ -0,0 +1,100 @@ +require 'observer' + +module ActiveModel + module Observing + module ClassMethods + def observers + @observers ||= [] + end + + def observers=(*values) + @observers = values.flatten + end + + def instantiate_observers + observers.each { |o| instantiate_observer(o) } + end + + protected + def instantiate_observer(observer) + # string/symbol + if observer.respond_to?(:to_sym) + observer = observer.to_s.camelize.constantize.instance + elsif observer.respond_to?(:instance) + observer.instance + else + raise ArgumentError, "#{observer} must be a lowercase, underscored class name (or an instance of the class itself) responding to the instance method. Example: Person.observers = :big_brother # calls BigBrother.instance" + end + end + + # Notify observers when the observed class is subclassed. + def inherited(subclass) + super + changed + notify_observers :observed_class_inherited, subclass + end + end + + def self.included(receiver) + receiver.extend Observable, ClassMethods + end + end + + class Observer + include Singleton + attr_writer :observed_classes + + class << self + attr_accessor :models + # Attaches the observer to the supplied model classes. + def observe(*models) + @models = models.flatten + @models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model } + end + + def observed_class_name + @observed_class_name ||= + if guessed_name = name.scan(/(.*)Observer/)[0] + @observed_class_name = guessed_name[0] + end + end + + # The class observed by default is inferred from the observer's class name: + # assert_equal [Person], PersonObserver.observed_class + def observed_class + if observed_class_name + observed_class_name.constantize + else + nil + end + end + end + + # Start observing the declared classes and their subclasses. + def initialize + self.observed_classes = self.class.models if self.class.models + observed_classes.each { |klass| add_observer! klass } + end + + # Send observed_method(object) if the method exists. + def update(observed_method, object) #:nodoc: + send(observed_method, object) if respond_to?(observed_method) + end + + # Special method sent by the observed class when it is inherited. + # Passes the new subclass. + def observed_class_inherited(subclass) #:nodoc: + self.class.observe(observed_classes + [subclass]) + add_observer!(subclass) + end + + protected + def observed_classes + @observed_classes ||= [self.class.observed_class] + end + + def add_observer!(klass) + klass.add_observer(self) + end + end +end \ No newline at end of file diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb new file mode 100644 index 0000000000..e3f6584e2b --- /dev/null +++ b/activemodel/lib/active_model/validations.rb @@ -0,0 +1,4 @@ +module ActiveModel + module Validations + end +end \ No newline at end of file diff --git a/activemodel/spec/observing_spec.rb b/activemodel/spec/observing_spec.rb new file mode 100644 index 0000000000..1919bb5991 --- /dev/null +++ b/activemodel/spec/observing_spec.rb @@ -0,0 +1,120 @@ +require File.join(File.dirname(__FILE__), 'spec_helper') + +class ObservedModel < ActiveModel::Base + class Observer + end +end + +class FooObserver < ActiveModel::Observer + class << self + public :new + end + + attr_accessor :stub + + def on_spec(record) + stub.event_with(record) if stub + end +end + +class Foo < ActiveModel::Base +end + +module ActiveModel + describe Observing do + before do + ObservedModel.observers.clear + end + + it "initializes model with no cached observers" do + ObservedModel.observers.should be_empty + end + + it "stores cached observers in an array" do + ObservedModel.observers << :foo + ObservedModel.observers.should include(:foo) + end + + it "flattens array of assigned cached observers" do + ObservedModel.observers = [[:foo], :bar] + ObservedModel.observers.should include(:foo) + ObservedModel.observers.should include(:bar) + end + + it "instantiates observer names passed as strings" do + ObservedModel.observers << 'foo_observer' + FooObserver.should_receive(:instance) + ObservedModel.instantiate_observers + end + + it "instantiates observer names passed as symbols" do + ObservedModel.observers << :foo_observer + FooObserver.should_receive(:instance) + ObservedModel.instantiate_observers + end + + it "instantiates observer classes" do + ObservedModel.observers << ObservedModel::Observer + ObservedModel::Observer.should_receive(:instance) + ObservedModel.instantiate_observers + end + + it "should pass observers to subclasses" do + FooObserver.instance + bar = Class.new(Foo) + bar.count_observers.should == 1 + end + end + + describe Observer do + before do + ObservedModel.observers = :foo_observer + FooObserver.models = nil + end + + it "guesses implicit observable model name" do + FooObserver.observed_class_name.should == 'Foo' + end + + it "tracks implicit observable models" do + instance = FooObserver.new + instance.send(:observed_classes).should include(Foo) + instance.send(:observed_classes).should_not include(ObservedModel) + end + + it "tracks explicit observed model class" do + FooObserver.new.send(:observed_classes).should_not include(ObservedModel) + FooObserver.observe ObservedModel + instance = FooObserver.new + instance.send(:observed_classes).should include(ObservedModel) + end + + it "tracks explicit observed model as string" do + FooObserver.new.send(:observed_classes).should_not include(ObservedModel) + FooObserver.observe 'observed_model' + instance = FooObserver.new + instance.send(:observed_classes).should include(ObservedModel) + end + + it "tracks explicit observed model as symbol" do + FooObserver.new.send(:observed_classes).should_not include(ObservedModel) + FooObserver.observe :observed_model + instance = FooObserver.new + instance.send(:observed_classes).should include(ObservedModel) + end + + it "calls existing observer event" do + foo = Foo.new + FooObserver.instance.stub = stub!(:stub) + FooObserver.instance.stub.should_receive(:event_with).with(foo) + Foo.send(:changed) + Foo.send(:notify_observers, :on_spec, foo) + end + + it "skips nonexistent observer event" do + foo = Foo.new + Foo.send(:changed) + Foo.send(:notify_observers, :whatever, foo) + end + end +end \ No newline at end of file diff --git a/activemodel/spec/spec_helper.rb b/activemodel/spec/spec_helper.rb new file mode 100644 index 0000000000..004fdfca07 --- /dev/null +++ b/activemodel/spec/spec_helper.rb @@ -0,0 +1,17 @@ +ENV['LOG_NAME'] = 'spec' +$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'vendor', 'rspec', 'lib') +$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib') +require 'active_model' +begin + require 'spec' +rescue LoadError + require 'rubygems' + require 'spec' +end + +begin + require 'ruby-debug' + Debugger.start +rescue LoadError + # you do not know the ways of ruby-debug yet, what a shame +end \ No newline at end of file