diff --git a/lib/dm-types.rb b/lib/dm-types.rb index 3c50b96..52c58ea 100644 --- a/lib/dm-types.rb +++ b/lib/dm-types.rb @@ -12,12 +12,27 @@ class Property autoload :IPAddress, 'dm-types/ip_address' autoload :Json, 'dm-types/json' autoload :Regexp, 'dm-types/regexp' - autoload :ParanoidBoolean, 'dm-types/paranoid_boolean' - autoload :ParanoidDateTime, 'dm-types/paranoid_datetime' + autoload :ParanoidBoolean, 'dm-types/destroyed/boolean' + autoload :ParanoidDateTime, 'dm-types/destroyed/date_time' autoload :Slug, 'dm-types/slug' autoload :UUID, 'dm-types/uuid' autoload :URI, 'dm-types/uri' autoload :Yaml, 'dm-types/yaml' autoload :APIKey, 'dm-types/api_key' - end -end + + module Created + autoload :Date, 'dm-types/created/date' + autoload :DateTime, 'dm-types/created/date_time' + end # module Created + + module Updated + autoload :Date, 'dm-types/updated/date' + autoload :DateTime, 'dm-types/updated/date_time' + end # module Updated + + module Destroyed + autoload :Boolean, 'dm-types/destroyed/boolean' + autoload :DateTime, 'dm-types/destroyed/date_time' + end # module Destroyed + end # class Property +end # module DataMapper diff --git a/lib/dm-types/created/date.rb b/lib/dm-types/created/date.rb new file mode 100644 index 0000000..dc3cba5 --- /dev/null +++ b/lib/dm-types/created/date.rb @@ -0,0 +1,21 @@ +require 'dm-types/support/date_stamp' +require 'dm-types/support/temporal_stamp_property' + +module DataMapper + class Property + module Created + class Date < ::DataMapper::Property::Date + include Types::Support::DateStamp + include Types::Support::TemporalStampProperty + + required true + auto_validation false if accepted_options.include?(:auto_validation) + + def stamp_resource(resource) + resource[name] ||= stamp_value if resource.new? + end + + end # class Date + end # module Created + end # class Property +end # module DataMapper diff --git a/lib/dm-types/created/date_time.rb b/lib/dm-types/created/date_time.rb new file mode 100644 index 0000000..8ae805d --- /dev/null +++ b/lib/dm-types/created/date_time.rb @@ -0,0 +1,21 @@ +require 'dm-types/support/date_time_stamp' +require 'dm-types/support/temporal_stamp_property' + +module DataMapper + class Property + module Created + class DateTime < ::DataMapper::Property::DateTime + include Types::Support::DateTimeStamp + include Types::Support::TemporalStampProperty + + required true + auto_validation false if accepted_options.include?(:auto_validation) + + def stamp_resource(resource) + resource[name] ||= stamp_value if resource.new? + end + + end # class DateTime + end # module Created + end # class Property +end # module DataMapper diff --git a/lib/dm-types/destroyed/boolean.rb b/lib/dm-types/destroyed/boolean.rb new file mode 100644 index 0000000..69b2109 --- /dev/null +++ b/lib/dm-types/destroyed/boolean.rb @@ -0,0 +1,21 @@ +require 'dm-types/support/paranoid_property' + +module DataMapper + class Property + module Destroyed + class Boolean < ::DataMapper::Property::Boolean + include Types::Support::ParanoidProperty + + default false + lazy true + + def stamp_value + true + end + + end # class Boolean + end # module Destroyed + + ParanoidBoolean = Destroyed::Boolean + end # module Property +end # module DataMapper diff --git a/lib/dm-types/destroyed/date_time.rb b/lib/dm-types/destroyed/date_time.rb new file mode 100644 index 0000000..c0b8274 --- /dev/null +++ b/lib/dm-types/destroyed/date_time.rb @@ -0,0 +1,21 @@ +require 'dm-types/support/paranoid_property' + +module DataMapper + class Property + module Destroyed + class DateTime < ::DataMapper::Property::DateTime + include Types::Support::ParanoidProperty + + default nil + lazy true + + def stamp_value + ::DateTime.now + end + + end # class DateTime + end # module Destroyed + + ParanoidDateTime = Destroyed::DateTime + end # module Property +end # module DataMapper diff --git a/lib/dm-types/enum.rb b/lib/dm-types/enum.rb index 3578ed4..973a4be 100644 --- a/lib/dm-types/enum.rb +++ b/lib/dm-types/enum.rb @@ -5,7 +5,7 @@ module DataMapper class Property class Enum < Integer - include Flags + include Types::Support::Flags def initialize(model, name, options = {}) super @@ -19,10 +19,8 @@ def initialize(model, name, options = {}) if defined?(::DataMapper::Validations) unless model.skip_auto_validation_for?(self) - if self.class.ancestors.include?(Property::Enum) - allowed = flag_map.values_at(*flag_map.keys.sort) - model.validates_within name, model.options_with_message({ :set => allowed }, self, :within) - end + allowed = flag_map.values_at(*flag_map.keys.sort) + model.validates_within name, model.options_with_message({ :set => allowed }, self, :within) end end end diff --git a/lib/dm-types/flag.rb b/lib/dm-types/flag.rb index f9a24d5..43ce396 100644 --- a/lib/dm-types/flag.rb +++ b/lib/dm-types/flag.rb @@ -5,7 +5,7 @@ module DataMapper class Property class Flag < Integer - include Flags + include Types::Support::Flags def initialize(model, name, options = {}) super diff --git a/lib/dm-types/paranoid/base.rb b/lib/dm-types/paranoid/base.rb deleted file mode 100644 index f7131c0..0000000 --- a/lib/dm-types/paranoid/base.rb +++ /dev/null @@ -1,55 +0,0 @@ -module DataMapper - module Types - module Paranoid - module Base - def self.included(model) - model.extend ClassMethods - model.instance_variable_set(:@paranoid_properties, {}) - end - - def paranoid_destroy - model.paranoid_properties.each do |name, block| - attribute_set(name, block.call(self)) - end - save_self - self.persisted_state = Resource::State::Immutable.new(self) - true - end - - private - - # @api private - def _destroy(execute_hooks = true) - return false unless saved? - if execute_hooks - paranoid_destroy - else - super - end - end - end # module Base - - module ClassMethods - def inherited(model) - model.instance_variable_set(:@paranoid_properties, @paranoid_properties.dup) - super - end - - # @api public - def with_deleted - with_exclusive_scope({}) { block_given? ? yield : all } - end - - # @api private - def paranoid_properties - @paranoid_properties - end - - # @api private - def set_paranoid_property(name, &block) - paranoid_properties[name] = block - end - end # module ClassMethods - end # module Paranoid - end # module Types -end # module DataMapper diff --git a/lib/dm-types/paranoid_boolean.rb b/lib/dm-types/paranoid_boolean.rb deleted file mode 100644 index b03364e..0000000 --- a/lib/dm-types/paranoid_boolean.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'dm-types/paranoid/base' - -module DataMapper - class Property - class ParanoidBoolean < Boolean - default false - lazy true - - # @api private - def bind - property_name = name.inspect - - model.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - include DataMapper::Types::Paranoid::Base - - set_paranoid_property(#{property_name}) { true } - - default_scope(#{repository_name.inspect}).update(#{property_name} => false) - RUBY - end - end # class ParanoidBoolean - end # module Property -end # module DataMapper diff --git a/lib/dm-types/paranoid_datetime.rb b/lib/dm-types/paranoid_datetime.rb deleted file mode 100644 index e70dafe..0000000 --- a/lib/dm-types/paranoid_datetime.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'dm-types/paranoid/base' - -module DataMapper - class Property - class ParanoidDateTime < DateTime - lazy true - - # @api private - def bind - property_name = name.inspect - - model.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - include DataMapper::Types::Paranoid::Base - - set_paranoid_property(#{property_name}) { ::DateTime.now } - - default_scope(#{repository_name.inspect}).update(#{property_name} => nil) - RUBY - end - end # class ParanoidDateTime - end # module Property -end # module DataMapper diff --git a/lib/dm-types/support/date_stamp.rb b/lib/dm-types/support/date_stamp.rb new file mode 100644 index 0000000..572a07d --- /dev/null +++ b/lib/dm-types/support/date_stamp.rb @@ -0,0 +1,13 @@ +module DataMapper + module Types + module Support + module DateStamp + + def stamp_value + ::Date.today + end + + end # module DateStamp + end # module Support + end # module Types +end # module DataMapper diff --git a/lib/dm-types/support/date_time_stamp.rb b/lib/dm-types/support/date_time_stamp.rb new file mode 100644 index 0000000..acb1143 --- /dev/null +++ b/lib/dm-types/support/date_time_stamp.rb @@ -0,0 +1,13 @@ +module DataMapper + module Types + module Support + module DateTimeStamp + + def stamp_value + ::DateTime.now + end + + end # module DateTimeStamp + end # module Support + end # module Types +end # module DataMapper diff --git a/lib/dm-types/support/flags.rb b/lib/dm-types/support/flags.rb index 1599b85..348e49a 100644 --- a/lib/dm-types/support/flags.rb +++ b/lib/dm-types/support/flags.rb @@ -1,41 +1,39 @@ module DataMapper - class Property - module Flags - def self.included(base) - base.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - extend DataMapper::Property::Flags::ClassMethods + module Types + module Support + module Flags + def self.included(model) + model.extend ClassMethods + model.accept_options :flags + model.__send__ :attr_reader, :flag_map + model.instance_variable_set(:@generated_classes, {}) - accept_options :flags - attr_reader :flag_map - - class << self + class << model attr_accessor :generated_classes end + end - self.generated_classes = {} - RUBY - end - - def custom? - true - end + def custom? + true + end - module ClassMethods - # TODO: document - # @api public - def [](*values) - if klass = generated_classes[values.flatten] - klass - else - klass = ::Class.new(self) - klass.flags(values) + module ClassMethods + # TODO: document + # @api public + def [](*values) + if klass = generated_classes[values.flatten] + klass + else + klass = ::Class.new(self) + klass.flags(values) - generated_classes[values.flatten] = klass + generated_classes[values.flatten] = klass - klass + klass + end end - end - end - end - end -end + end # module ClassMethods + end # module Flags + end # module Support + end # module Types +end # module DataMapper diff --git a/lib/dm-types/support/paranoid_property.rb b/lib/dm-types/support/paranoid_property.rb new file mode 100644 index 0000000..ac4e4e2 --- /dev/null +++ b/lib/dm-types/support/paranoid_property.rb @@ -0,0 +1,25 @@ +require "dm-types/support/paranoid_resource" + +module DataMapper + module Types + module Support + module ParanoidProperty + + # @api private + def bind + unless model < DataMapper::Types::Support::ParanoidResource + model.__send__(:include, DataMapper::Types::Support::ParanoidResource) + end + + model.paranoid_properties << self + model.default_scope(repository_name).update(name => self.class.default) + end + + def stamp_resource(resource) + resource[name] = stamp_value + end + + end # module UpdateStamp + end # module Support + end # class Property +end # module DataMapper diff --git a/lib/dm-types/support/paranoid_resource.rb b/lib/dm-types/support/paranoid_resource.rb new file mode 100644 index 0000000..ef4c82d --- /dev/null +++ b/lib/dm-types/support/paranoid_resource.rb @@ -0,0 +1,50 @@ +module DataMapper + module Types + module Support + module ParanoidResource + def self.included(model) + model.extend ClassMethods + model.instance_variable_set(:@paranoid_properties, [].to_set) + end + + private + + # @api private + def _destroy(execute_hooks = true) + if !saved? + false + elsif execute_hooks + paranoid_destroy + else + super + end + end + + # @api private + def paranoid_destroy + model.paranoid_properties.each do |property| + property.stamp_resource(self) + end + save_self + self.persisted_state = Resource::State::Immutable.new(self) + true + end + + module ClassMethods + # @api private + attr_reader :paranoid_properties + + def inherited(model) + model.instance_variable_set(:@paranoid_properties, @paranoid_properties.dup) + super + end + + # @api public + def with_deleted + with_exclusive_scope({}) { block_given? ? yield : all } + end + end # module ClassMethods + end # module Paranoia + end # module Support + end # module Types +end # module DataMapper diff --git a/lib/dm-types/support/temporal_stamp_property.rb b/lib/dm-types/support/temporal_stamp_property.rb new file mode 100644 index 0000000..d10b03c --- /dev/null +++ b/lib/dm-types/support/temporal_stamp_property.rb @@ -0,0 +1,23 @@ +require "dm-types/support/temporally_stamped_resource" + +module DataMapper + module Types + module Support + module TemporalStampProperty + + def bind + unless model < TemporallyStampedResource + model.__send__ :include, TemporallyStampedResource + end + + model.temporally_stamped_properties << self + end + + def stamp_resource(resource) + resource[name] = stamp_value + end + + end # module CreateStamp + end # module Support + end # class Property +end # module DataMapper diff --git a/lib/dm-types/support/temporally_stamped_resource.rb b/lib/dm-types/support/temporally_stamped_resource.rb new file mode 100644 index 0000000..e387f7d --- /dev/null +++ b/lib/dm-types/support/temporally_stamped_resource.rb @@ -0,0 +1,41 @@ +module DataMapper + module Types + module Support + module TemporallyStampedResource + + def self.included(model) + model.before :save, :set_temporal_stamps + model.instance_variable_set(:@temporally_stamped_properties, [].to_set) + model.extend ClassMethods + end + + # Saves the record with the updated_at/on attributes set to the current time. + def touch + set_temporal_stamps(true) + save + end + + private + + def set_temporal_stamps(set_stamps = self.dirty?) + return unless set_stamps + + model.temporally_stamped_properties.each do |property| + property.stamp_resource(self) + end + end + + module ClassMethods + attr_reader :temporally_stamped_properties + + def inherited(model) + model.instance_variable_set :@temporally_stamped_properties, + @temporally_stamped_properties.dup + super + end + end # module ClassMethods + + end # module Timestamp + end # module Support + end # class Property +end # module DataMapper diff --git a/lib/dm-types/updated/date.rb b/lib/dm-types/updated/date.rb new file mode 100644 index 0000000..c5f449d --- /dev/null +++ b/lib/dm-types/updated/date.rb @@ -0,0 +1,17 @@ +require 'dm-types/support/date_stamp' +require 'dm-types/support/temporal_stamp_property' + +module DataMapper + class Property + module Updated + class Date < ::DataMapper::Property::Date + include Types::Support::DateStamp + include Types::Support::TemporalStampProperty + + required true + auto_validation false if accepted_options.include?(:auto_validation) + + end # class Date + end # module Updated + end # class Property +end # module DataMapper diff --git a/lib/dm-types/updated/date_time.rb b/lib/dm-types/updated/date_time.rb new file mode 100644 index 0000000..d9417b2 --- /dev/null +++ b/lib/dm-types/updated/date_time.rb @@ -0,0 +1,17 @@ +require 'dm-types/support/date_time_stamp' +require 'dm-types/support/temporal_stamp_property' + +module DataMapper + class Property + module Updated + class DateTime < ::DataMapper::Property::DateTime + include Types::Support::DateTimeStamp + include Types::Support::TemporalStampProperty + + required true + auto_validation false if accepted_options.include?(:auto_validation) + + end # class DateTime + end # module Updated + end # class Property +end # module DataMapper diff --git a/spec/integration/timestamps_spec.rb b/spec/integration/timestamps_spec.rb new file mode 100644 index 0000000..024c907 --- /dev/null +++ b/spec/integration/timestamps_spec.rb @@ -0,0 +1,201 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe 'DataMapper::Timestamp' do + + supported_by :all do + + describe "Timestamp (shared behavior)", :shared => true do + it "should not set the created_at/on fields if they're already set" do + green_smoothie = GreenSmoothie.new(:name => 'Banana') + time = (DateTime.now - 100) + green_smoothie.created_at = time + green_smoothie.created_on = time + green_smoothie.save + green_smoothie.created_at.should == time + green_smoothie.created_on.should == time + green_smoothie.created_at.should be_a_kind_of(DateTime) + green_smoothie.created_on.should be_a_kind_of(Date) + end + + it "should set the created_at/on fields on creation" do + green_smoothie = GreenSmoothie.new(:name => 'Banana') + green_smoothie.created_at.should be_nil + green_smoothie.created_on.should be_nil + green_smoothie.save + green_smoothie.created_at.should be_a_kind_of(DateTime) + green_smoothie.created_on.should be_a_kind_of(Date) + end + + it "should not alter the create_at/on fields on model updates" do + green_smoothie = GreenSmoothie.new(:id => 2, :name => 'Berry') + green_smoothie.created_at.should be_nil + green_smoothie.created_on.should be_nil + green_smoothie.save + original_created_at = green_smoothie.created_at + original_created_on = green_smoothie.created_on + green_smoothie.name = 'Strawberry' + green_smoothie.save + green_smoothie.created_at.should eql(original_created_at) + green_smoothie.created_on.should eql(original_created_on) + end + + it "should set the updated_at/on fields on creation and on update" do + green_smoothie = GreenSmoothie.new(:name => 'Mango') + green_smoothie.updated_at.should be_nil + green_smoothie.updated_on.should be_nil + green_smoothie.save + green_smoothie.updated_at.should be_a_kind_of(DateTime) + green_smoothie.updated_on.should be_a_kind_of(Date) + original_updated_at = green_smoothie.updated_at + original_updated_on = green_smoothie.updated_on + time_tomorrow = DateTime.now + 1 + date_tomorrow = Date.today + 1 + DateTime.stub!(:now).and_return { time_tomorrow } + Date.stub!(:today).and_return { date_tomorrow } + green_smoothie.name = 'Cranberry Mango' + green_smoothie.save + green_smoothie.updated_at.should_not eql(original_updated_at) + green_smoothie.updated_on.should_not eql(original_updated_on) + green_smoothie.updated_at.should eql(time_tomorrow) + green_smoothie.updated_on.should eql(date_tomorrow) + end + + it "should only set the updated_at/on fields on dirty objects" do + green_smoothie = GreenSmoothie.new(:name => 'Mango') + green_smoothie.updated_at.should be_nil + green_smoothie.updated_on.should be_nil + green_smoothie.save + green_smoothie.updated_at.should be_a_kind_of(DateTime) + green_smoothie.updated_on.should be_a_kind_of(Date) + original_updated_at = green_smoothie.updated_at + original_updated_on = green_smoothie.updated_on + time_tomorrow = DateTime.now + 1 + date_tomorrow = Date.today + 1 + DateTime.stub!(:now).and_return { time_tomorrow } + Date.stub!(:today).and_return { date_tomorrow } + green_smoothie.save + green_smoothie.updated_at.should_not eql(time_tomorrow) + green_smoothie.updated_on.should_not eql(date_tomorrow) + green_smoothie.updated_at.should eql(original_updated_at) + green_smoothie.updated_on.should eql(original_updated_on) + end + + describe '#touch' do + it 'should update the updated_at/on fields' do + green_smoothie = GreenSmoothie.create(:name => 'Mango') + + time_tomorrow = DateTime.now + 1 + date_tomorrow = Date.today + 1 + DateTime.stub!(:now).and_return { time_tomorrow } + Date.stub!(:today).and_return { date_tomorrow } + + green_smoothie.touch + + green_smoothie.updated_at.should eql(time_tomorrow) + green_smoothie.updated_on.should eql(date_tomorrow) + end + + it 'should not update the created_at/on fields' do + green_smoothie = GreenSmoothie.create(:name => 'Mango') + + original_created_at = green_smoothie.created_at + original_created_on = green_smoothie.created_on + + green_smoothie.touch + + green_smoothie.created_at.should equal(original_created_at) + green_smoothie.created_on.should equal(original_created_on) + end + end + end + + describe "explicit property declaration" do + before do + Object.send(:remove_const, :GreenSmoothie) if defined?(GreenSmoothie) + class GreenSmoothie + include DataMapper::Resource + + property :id, Serial + property :name, String + property :created_at, Created::DateTime, :required => true + property :created_on, Created::Date, :required => true + property :updated_at, Updated::DateTime, :required => true + property :updated_on, Updated::Date, :required => true + + auto_migrate! + end + end + + it_should_behave_like "Timestamp (shared behavior)" + end + + # describe "implicit property declaration" do + # before do + # Object.send(:remove_const, :GreenSmoothie) if defined?(GreenSmoothie) + # class GreenSmoothie + # include DataMapper::Resource + # + # property :id, Serial + # property :name, String + # + # timestamps :at, :on + # + # auto_migrate! + # end + # end + # + # it_should_behave_like "Timestamp (shared behavior)" + # end + + # describe "timestamps helper" do + # describe "inclusion" do + # before :each do + # @klass = Class.new do + # include DataMapper::Resource + # end + # end + # + # it "should provide #timestamps" do + # @klass.should respond_to(:timestamps) + # end + # + # it "should set the *at properties" do + # @klass.timestamps :at + # + # @klass.properties.should be_named(:created_at) + # @klass.properties[:created_at].should be_kind_of(DataMapper::Property::DateTime) + # @klass.properties.should be_named(:updated_at) + # @klass.properties[:updated_at].should be_kind_of(DataMapper::Property::DateTime) + # end + # + # it "should set the *on properties" do + # @klass.timestamps :on + # + # @klass.properties.should be_named(:created_on) + # @klass.properties[:created_on].should be_kind_of(DataMapper::Property::Date) + # @klass.properties.should be_named(:updated_on) + # @klass.properties[:updated_on].should be_kind_of(DataMapper::Property::Date) + # end + # + # it "should set multiple properties" do + # @klass.timestamps :created_at, :updated_on + # + # @klass.properties.should be_named(:created_at) + # @klass.properties.should be_named(:updated_on) + # end + # + # it "should fail on unknown property name" do + # lambda { @klass.timestamps :wowee }.should raise_error(DataMapper::Timestamp::InvalidTimestampName) + # end + # + # it "should fail on empty arguments" do + # lambda { @klass.timestamps }.should raise_error(ArgumentError) + # end + # end + # end + + end + +end