Permalink
Browse files

Initial import

  • Loading branch information...
0 parents commit f4193da77d983b817c698c1fd05524a84bc7ac0e @solnic committed Jun 4, 2011
4 .gitignore
@@ -0,0 +1,4 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
9 Gemfile
@@ -0,0 +1,9 @@
+source "http://rubygems.org"
+
+gem 'virtus', '~> 0.0.1'
+
+group :development do
+ gem "jeweler", "~> 1.5.2"
+ gem "rspec", "~> 2.6.0"
+ gem "simplecov", "~> 0.4.2", :platforms => [ :mri_19 ]
+end
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2011 Piotr Solnica
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
42 README.rdoc
@@ -0,0 +1,42 @@
+= Virtus - Dirty Tracking
+
+Support for dirty tracking of virtus attributes.
+
+== Usage
+
+ class Post
+ include Virtus
+ include Virtus::DirtyTracking
+
+ attribute :title, String
+ attribute, :content, String
+ attribute, :meta, Hash
+ end
+
+ post = Post.new(:title => 'Foo', :meta => { :tags => ['red', 'green'] })
+
+ post.title = 'Bar'
+
+ post.dirty? # => true
+
+ post.attribute_dirty?(:title) # => true
+
+ post.meta[:tags] << 'blue'
+
+ post.attribute_dirty?(:meta) # => true
+
+ post.dirty_attributes # => {:title => 'Bar', :meta=>{:tags=>["red", "green", "blue"]}}
+
+== Note on Patches/Pull Requests
+
+* Fork the project.
+* Make your feature addition or bug fix.
+* Add tests for it. This is important so I don't break it in a
+ future version unintentionally.
+* Commit, do not mess with rakefile, version, or history.
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
+* Send me a pull request. Bonus points for topic branches.
+
+== Copyright
+
+Copyright (c) 2011 Piotr Solnica. See LICENSE for details.
1 Rakefile
@@ -0,0 +1 @@
+require 'bundler/gem_tasks'
1 VERSION
@@ -0,0 +1 @@
+0.0.1
128 lib/virtus/dirty_tracking.rb
@@ -0,0 +1,128 @@
+require 'virtus'
+require Pathname(__FILE__).dirname.expand_path + 'virtus/dirty_tracking/session'
+
+module Virtus
+ # == Dirty Tracking
+ #
+ # Dirty Tracking is an optional module that you include only if you need it.
+ module DirtyTracking
+ # Extends a class with DirtyTracking::Attributes module
+ #
+ # @param [Class] base
+ #
+ # @api private
+ def self.included(base)
+ base.extend(DirtyTracking::Attributes)
+ end
+
+ # Returns if an object is dirty
+ #
+ # @return [TrueClass, FalseClass]
+ #
+ # @api public
+ def dirty?
+ dirty_session.dirty?
+ end
+
+ # Returns if an attribute with the given name is dirty.
+ #
+ # @param [Symbol] name
+ #
+ # @return [TrueClass, FalseClass]
+ #
+ # @api public
+ def attribute_dirty?(name)
+ dirty_session.dirty?(name)
+ end
+
+ # Explicitly sets an attribute as dirty.
+ #
+ # @param [Symbol] name
+ # the name of an attribute
+ #
+ # @param [Object] value
+ # a value of an attribute
+ #
+ # @api public
+ def attribute_dirty!(name, value)
+ dirty_session.dirty!(name, value)
+ end
+
+ # Returns all dirty attributes
+ #
+ # @return [Hash]
+ # a hash indexed with attribute names
+ #
+ # @api public
+ def dirty_attributes
+ dirty_session.dirty_attributes
+ end
+
+ # Returns original attributes
+ #
+ # @return [Hash]
+ # a hash indexed with attribute names
+ #
+ # @api public
+ def original_attributes
+ dirty_session.original_attributes
+ end
+
+ # Returns the current dirty tracking session
+ #
+ # @return [Virtus::DirtyTracking::Session]
+ #
+ # @api private
+ def dirty_session
+ @_dirty_session ||= Session.new(self)
+ end
+
+ module Attributes
+ # Creates an attribute writer with dirty tracking
+ #
+ # @see Virtus::Attributes.attribute
+ #
+ # @return [Virtus::Attributes::Object]
+ #
+ # @api public
+ def attribute(name, type, options = {})
+ _create_writer_with_dirty_tracking(name, attribute = super)
+ attribute
+ end
+
+ private
+
+ # Creates an attribute writer method with dirty tracking
+ #
+ # @param [Symbol] name
+ # the name of an attribute
+ #
+ # @param [Virtus::Attributes::Object] attribute
+ # an attribute instance
+ #
+ # @api private
+ def _create_writer_with_dirty_tracking(name, attribute)
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ chainable(:dirty_tracking) do
+ #{attribute.writer_visibility}
+
+ def #{name}=(value)
+ prev_value = #{name}
+ new_value = super
+
+ if prev_value != new_value
+ unless original_attributes.key?(:#{name})
+ original_attributes[:#{name}] = prev_value
+ end
+
+ attribute_dirty!(:#{name}, new_value)
+ end
+
+ new_value
+ end
+ end
+ RUBY
+ end
+ end # Attributes
+ end # DirtyTracking
+end # Virtus
94 lib/virtus/dirty_tracking/session.rb
@@ -0,0 +1,94 @@
+module Virtus
+ module DirtyTracking
+ class Session
+ attr_reader :subject
+
+ # @api semipublic
+ def initialize(subject)
+ @subject = subject
+ end
+
+ # Returns original attributes of the subject
+ #
+ # @return [Hash]
+ # a hash of attributes indexed by attribute names
+ #
+ # @api semipublic
+ def original_attributes
+ @_original_attributes ||= subject.attributes.dup
+ end
+
+ # Returns dirty attributes of the subject
+ #
+ # @return [Hash]
+ # a hash of attributes indexed by attribute names
+ #
+ # @api semipublic
+ def dirty_attributes
+ (@_dirty_attributes ||= {}).update(complex_attributes)
+ end
+
+ # Sets an attribute as dirty
+ #
+ # @param [Symbol] name
+ # the name of an attribute
+ #
+ # @param [Object] value
+ # the value of an attribute
+ #
+ # @api semipublic
+ def dirty!(name, value)
+ dirty_attributes[name] = value
+ end
+
+ # Returns if an object is dirty or if an attribute with the given name is
+ # dirty.
+ #
+ # @param [Symbol] name
+ # the name of an attribute
+ #
+ # @return [TrueClass, FalseClass]
+ #
+ # @api semipublic
+ def dirty?(name = nil)
+ name ? dirty_attributes.key?(name) : dirty_attributes.any?
+ end
+
+ private
+
+ # Returns a values of complex dirty attributes that can be modified via
+ # their own APIs like Hash[] or Array<<
+ #
+ # @return [Hash]
+ # an attributes hash indexed by attribute names
+ #
+ # @api private
+ def complex_attributes
+ values = {}
+
+ complex_attributes_set.each do |name, attribute|
+ value = subject.__send__(name)
+
+ if original_attributes[name] != value
+ values[name] = value
+ end
+ end
+
+ values
+ end
+
+ # Returns a hash of complex attribute instances defined on
+ # the subject's class.
+ #
+ # @return [Hash]
+ # the attribute instances hash indexed by attribute names
+ #
+ # @api private
+ def complex_attributes_set
+ @_complex_attributes_set ||= subject.class.attributes.select do |name, attribute|
+ attribute.complex?
+ end
+ end
+ end # Session
+ end # DirtyTracking
+end # Virtus
5 lib/virtus/dirty_tracking/version.rb
@@ -0,0 +1,5 @@
+module Virtus
+ module DirtyTracking
+ VERSION = "0.0.1"
+ end
+end
38 spec/integration/attributes/array_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Virtus::Attributes::Array do
+ it_should_behave_like 'Dirty Trackable Attribute' do
+ let(:attribute_name) { :colors }
+ let(:attribute_value) { [ 'red', 'green', 'blue' ] }
+ let(:attribute_value_other) { [ 'orange', 'yellow', 'gray' ] }
+ end
+
+ describe 'dirty tracking' do
+ let(:model) do
+ Class.new do
+ include Virtus
+ include Virtus::DirtyTracking
+
+ attribute :colors, Array
+ end
+ end
+
+ let(:object) do
+ model.new(:colors => [])
+ end
+
+ context "when value is set implicitly" do
+ before do
+ object.colors << 'gray'
+ end
+
+ it "marks the attribute as dirty" do
+ object.attribute_dirty?(:colors).should be(true)
+ end
+
+ it "sets dirty attributes hash" do
+ object.dirty_attributes.should == { :colors => ['gray'] }
+ end
+ end
+ end
+end
9 spec/integration/attributes/boolean_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe Virtus::Attributes::Boolean do
+ it_should_behave_like "Dirty Trackable Attribute" do
+ let(:attribute_name) { :is_admin }
+ let(:attribute_value) { true }
+ let(:attribute_value_other) { '1' }
+ end
+end
9 spec/integration/attributes/date_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe Virtus::Attributes::Date do
+ it_should_behave_like 'Dirty Trackable Attribute' do
+ let(:attribute_name) { :created_on }
+ let(:attribute_value) { Date.today }
+ let(:attribute_value_other) { (Date.today+1).to_s }
+ end
+end
9 spec/integration/attributes/date_time_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe Virtus::Attributes::DateTime do
+ it_should_behave_like 'Dirty Trackable Attribute' do
+ let(:attribute_name) { :created_at }
+ let(:attribute_value) { DateTime.now }
+ let(:attribute_value_other) { DateTime.now.to_s }
+ end
+end
9 spec/integration/attributes/decimal_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe Virtus::Attributes::Decimal do
+ it_should_behave_like 'Dirty Trackable Attribute' do
+ let(:attribute_name) { :price }
+ let(:attribute_value) { BigDecimal("12.3456789") }
+ let(:attribute_value_other) { "12.3456789" }
+ end
+end
9 spec/integration/attributes/float_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe Virtus::Attributes::Float do
+ it_should_behave_like 'Dirty Trackable Attribute' do
+ let(:attribute_name) { :score }
+ let(:attribute_value) { 12.34 }
+ let(:attribute_value_other) { "12.34" }
+ end
+end
38 spec/integration/attributes/hash_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Virtus::Attributes::Hash do
+ it_should_behave_like 'Dirty Trackable Attribute' do
+ let(:attribute_name) { :settings }
+ let(:attribute_value) { Hash[:one => 1] }
+ let(:attribute_value_other) { Hash[:two => 2] }
+ end
+
+ describe 'dirty tracking' do
+ let(:model) do
+ Class.new do
+ include Virtus
+ include Virtus::DirtyTracking
+
+ attribute :settings, Hash
+ end
+ end
+
+ let(:object) do
+ model.new(:settings => {})
+ end
+
+ context "when value is set implicitly" do
+ before do
+ object.settings[:one] = '1'
+ end
+
+ it "marks the attribute as dirty" do
+ object.attribute_dirty?(:settings).should be(true)
+ end
+
+ it "sets dirty attributes hash" do
+ object.dirty_attributes.should == { :settings => { :one => '1' } }
+ end
+ end
+ end
+end
9 spec/integration/attributes/integer_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe Virtus::Attributes::Integer do
+ it_should_behave_like 'Dirty Trackable Attribute' do
+ let(:attribute_name) { :age }
+ let(:attribute_value) { 28 }
+ let(:attribute_value_other) { "28" }
+ end
+end
9 spec/integration/attributes/string_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe Virtus::Attributes::String do
+ it_should_behave_like 'Dirty Trackable Attribute' do
+ let(:attribute_name) { :email }
+ let(:attribute_value) { 'red john' }
+ let(:attribute_value_other) { :'red john' }
+ end
+end
9 spec/integration/attributes/time_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe Virtus::Attributes::Time do
+ it_should_behave_like 'Dirty Trackable Attribute' do
+ let(:attribute_name) { :birthday }
+ let(:attribute_value) { Time.now }
+ let(:attribute_value_other) { Time.now.to_s }
+ end
+end
72 spec/integration/shared/dirty_trackable_attribute.rb
@@ -0,0 +1,72 @@
+shared_examples_for "Dirty Trackable Attribute" do
+ let(:model) do
+ model = Class.new do
+ include Virtus
+ include Virtus::DirtyTracking
+ end
+ model.attribute attribute_name, described_class
+ model
+ end
+
+ context "when object is clean" do
+ let(:object) do
+ model.new
+ end
+
+ it "doesn't mark it as dirty" do
+ object.dirty?.should be(false)
+ end
+ end
+
+ context "when value is set on a new object" do
+ let(:object) do
+ model.new
+ end
+
+ before do
+ object.attribute_set(attribute_name, attribute_value)
+ end
+
+ it "marks the object as dirty" do
+ object.dirty?.should be(true)
+ end
+
+ it "marks the attribute as dirty" do
+ object.attribute_dirty?(attribute_name).should be(true)
+ end
+
+ it "sets new value in dirty_attributes hash" do
+ object.dirty_attributes[attribute_name].should == attribute_value
+ end
+ end
+
+ context "when other value is set on a new object with attribute already set" do
+ let(:object) do
+ model.new(attribute_name => attribute_value)
+ end
+
+ let(:new_value) do
+ model.attributes[attribute_name].typecast(attribute_value_other)
+ end
+
+ before do
+ object.attribute_set(attribute_name, new_value)
+ end
+
+ it "marks the object as dirty" do
+ object.dirty?.should be(true)
+ end
+
+ it "marks the attribute as dirty" do
+ object.attribute_dirty?(attribute_name).should be(true)
+ end
+
+ it "sets new value in dirty_attributes hash" do
+ object.dirty_attributes[attribute_name].should == new_value
+ end
+
+ it "sets original value" do
+ object.original_attributes[attribute_name].should == attribute_value
+ end
+ end
+end
27 virtus-dirty_tracking.gemspec
@@ -0,0 +1,27 @@
+require 'rubygems'
+require 'rake'
+require 'jeweler'
+require 'rspec/core/rake_task'
+
+Jeweler::Tasks.new do |gem|
+ gem.name = "virtus-dirty_tracking"
+ gem.platform = Gem::Platform::RUBY
+ gem.authors = ["Piotr Solnica"]
+ gem.email = ["piotr@rubyverse.com"]
+ gem.homepage = "https://github.com/solnic/virtus-dirty_tracking"
+ gem.summary = %q{Add dirty tracking of attributes to your ruby objects}
+ gem.description = gem.summary
+
+ gem.files = `git ls-files`.split("\n")
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ gem.require_paths = ["lib"]
+end
+
+Jeweler::GemcutterTasks.new
+
+desc "Run specs"
+RSpec::Core::RakeTask.new
+
+desc 'Default: run specs.'
+task :default => :spec

3 comments on commit f4193da

@rrychu

Jeweler? WTF.

@solnic
Owner

@rrychu echo from the past...it's going away soon...also this project is just a code spike

@rrychu

Oh. That's cool bro.

Please sign in to comment.