├── .bundle └── config ├── .rvmrc ├── .gitignore ├── examples ├── common.rb ├── auditer.rb ├── active_model.rb └── active_record.rb ├── lib ├── audit.rb └── audit │ ├── changeset.rb │ ├── log.rb │ └── tracking.rb ├── Gemfile ├── test ├── storage-conf.xml ├── helper.rb ├── ci-build ├── test_changeset.rb ├── test_log.rb └── test_tracking.rb ├── LICENSE ├── README.md ├── Rakefile └── audit.gemspec /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_WITHOUT: "" 3 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use ree-1.8.7-2011.03@audit --create 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | rdoc/Gemfile.lock 3 | Gemfile.lock 4 | -------------------------------------------------------------------------------- /examples/common.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.setup 3 | -------------------------------------------------------------------------------- /examples/auditer.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | require 'audit' 3 | 4 | if __FILE__ == $PROGRAM_NAME 5 | log = Audit::Log 6 | log.connection = Cassandra.new('Audit') 7 | 8 | changes = {:age => [30, 31]} 9 | log.record(:user, 1, changes) 10 | p log.audits(:user, 1) 11 | end 12 | -------------------------------------------------------------------------------- /lib/audit.rb: -------------------------------------------------------------------------------- 1 | # Audit is a system for tracking model changes outside of your application's 2 | # database. 3 | module Audit 4 | 5 | # Everything needs a version. 6 | VERSION = '0.7.3' 7 | 8 | autoload :Log, "audit/log" 9 | autoload :Changeset, "audit/changeset" 10 | autoload :Tracking, "audit/tracking" 11 | 12 | end 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :gemcutter 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "activerecord", "~> 3.0.0" 7 | gem "sqlite3-ruby" 8 | gem "activemodel", "~> 3.0.0" 9 | end 10 | 11 | group :test do 12 | gem "shoulda", "~> 2.11.3" 13 | gem "nokogiri", "~> 1.4.3.1" # Cassandra::Mock needs this 14 | gem "flexmock", "~> 0.8.7" 15 | end 16 | -------------------------------------------------------------------------------- /test/storage-conf.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0.01 5 | 6 | org.apache.cassandra.locator.RackUnawareStrategy 7 | 1 8 | org.apache.cassandra.locator.EndPointSnitch 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/active_model.rb: -------------------------------------------------------------------------------- 1 | # TODO: write me 2 | 3 | require 'common' 4 | require 'audit' 5 | require 'active_model' 6 | 7 | class User 8 | extend ActiveModel::Callbacks 9 | 10 | define_model_callbacks :create, :update 11 | after_update :bonk 12 | after_create :clown 13 | 14 | def self.create(attrs) 15 | new.create(attrs) 16 | end 17 | 18 | def create(attrs) 19 | _run_create_callbacks do 20 | puts "Creating: #{attrs}" 21 | end 22 | end 23 | 24 | def update(attrs) 25 | _run_update_callbacks do 26 | puts "Updating #{attrs}" 27 | end 28 | end 29 | 30 | def bonk 31 | puts 'bonk!' 32 | end 33 | 34 | def clown 35 | puts "clown!" 36 | end 37 | 38 | end 39 | 40 | if __FILE__ == $PROGRAM_NAME 41 | User.create(:foo) 42 | end 43 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | Bundler.setup 4 | 5 | require 'test/unit' 6 | require 'shoulda' 7 | require 'flexmock/test_unit' 8 | require 'cassandra/0.7' 9 | require 'cassandra/mock' 10 | require 'active_record' 11 | require 'audit' 12 | 13 | if ENV["CASSANDRA"] == "Y" 14 | puts "-- RUNNING TESTS AGAINST CASSANDRA --" 15 | end 16 | 17 | class Test::Unit::TestCase 18 | 19 | alias_method :original_setup, :setup 20 | def setup 21 | if ENV["CASSANDRA"] == "Y" 22 | Audit::Log.connection = Cassandra.new("Audit") 23 | Audit::Log.connection.clear_keyspace! 24 | else 25 | schema = { 26 | 'Audit' => { 27 | 'Audits' => {} 28 | } 29 | } 30 | Audit::Log.connection = Cassandra::Mock.new('Audit', schema) 31 | end 32 | original_setup 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /test/ci-build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | desired_ruby=ree-1.8.7-2011.03 4 | gemset_name=audit 5 | 6 | # enable rvm for ruby interpreter switching 7 | source $HOME/.rvm/scripts/rvm || exit 1 8 | 9 | # show available (installed) rubies (for debugging) 10 | #rvm list 11 | 12 | # install our chosen ruby if necessary 13 | rvm list | grep $desired_ruby > /dev/null || rvm install $desired_ruby || exit 1 14 | 15 | # use our ruby with a custom gemset 16 | rvm use ${desired_ruby}@${gemset_name} --create 17 | 18 | # install bundler if necessary 19 | gem list --local bundler | grep bundler || gem install bundler || exit 1 20 | 21 | # debugging info 22 | echo USER=$USER && ruby --version && which ruby && which bundle 23 | 24 | # conditionally install project gems from Gemfile 25 | bundle check || bundle install || exit 1 26 | 27 | rake db:migrate 28 | 29 | CASSANDRA=Y rake test 30 | -------------------------------------------------------------------------------- /examples/active_record.rb: -------------------------------------------------------------------------------- 1 | require 'common' 2 | require 'pp' 3 | require 'audit' 4 | require 'active_record' 5 | 6 | ActiveRecord::Base.establish_connection( 7 | :adapter => 'sqlite3', 8 | :database => ':memory:' 9 | ) 10 | 11 | ActiveRecord::Schema.define do 12 | create_table :users do |t| 13 | t.string :username, :null => false 14 | t.integer :age, :null => false 15 | t.integer :gizmo 16 | end 17 | end 18 | 19 | class User < ActiveRecord::Base 20 | include Audit::Tracking 21 | end 22 | 23 | if __FILE__ == $PROGRAM_NAME 24 | Audit::Log.connection = Cassandra.new('Audit') 25 | Audit::Log.clear! 26 | 27 | user = User.create(:username => 'adam', :age => 30) 28 | user.update_attributes(:age => 31) 29 | user.update_attributes(:username => 'akk') 30 | 31 | user.audit_metadata(:reason => "Canonize usernames") 32 | user.update_attributes(:username => 'therealadam') 33 | 34 | 100.times.each { |i| user.update_attributes(:gizmo => i) } 35 | 36 | pp user.audits 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Adam Keys 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /test/test_changeset.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class ChangesetTest < Test::Unit::TestCase 4 | 5 | should 'convert a hash of changes to a changeset' do 6 | metadata = { 7 | "reason" => "Canonize usernames, getting older", 8 | "signed" => "akk" 9 | } 10 | changes = { 11 | "changes" => { 12 | "username" => ["akk", "adam"], 13 | "age" => [30, 31] 14 | }, 15 | "metadata" => metadata 16 | } 17 | changeset = Audit::Changeset.from_enumerable(changes) 18 | 19 | assert_equal 2, changeset.changes.length 20 | assert(changeset.changes.all? { |cs| 21 | %w{username age}.include?(cs.attribute) 22 | ["akk", 30].include?(cs.old_value) 23 | ["adam", 31].include?(cs.new_value) 24 | }) 25 | assert_equal metadata, changeset.metadata 26 | end 27 | 28 | should "convert multile change records to an Array of Changesets" do 29 | changes = [ 30 | { 31 | "changes" => {"username" => ["akk", "adam"], "age" => [30, 31]}, 32 | "metadata" => {} 33 | }, 34 | { 35 | "changes" => { 36 | "username" => ["adam", "therealadam"], 37 | "age" => [31, 32] 38 | }, 39 | "metadata" => {} 40 | } 41 | 42 | ] 43 | changesets = Audit::Changeset.from_enumerable(changes) 44 | 45 | assert_equal 2, changesets.length 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /test/test_log.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class LogTest < Test::Unit::TestCase 4 | 5 | should "save audit record" do 6 | assert_nothing_raised do 7 | Audit::Log.record(:Users, 1, timestamp, simple_change) 8 | end 9 | end 10 | 11 | should "save audit records idempotently" do 12 | t = Time.now.utc.iso8601 13 | 3.times { Audit::Log.record(:Users, 1, t, simple_change) } 14 | assert_equal 1, Audit::Log.audits(:Users, 1).length 15 | end 16 | 17 | should "load audit records" do 18 | Audit::Log.record(:Users, 1, timestamp, simple_change) 19 | assert_kind_of Audit::Changeset, Audit::Log.audits(:Users, 1).first 20 | end 21 | 22 | should "load audits with multiple changed attributes" do 23 | Audit::Log.record(:Users, 1, timestamp, multiple_changes) 24 | changes = Audit::Log.audits(:Users, 1).first.changes 25 | changes.each do |change| 26 | assert %w{username age}.include?(change.attribute) 27 | assert ["akk", 30].include?(change.old_value) 28 | assert ["adam", 31].include?(change.new_value) 29 | end 30 | end 31 | 32 | def timestamp 33 | Time.now.utc.iso8601 34 | end 35 | 36 | def simple_change 37 | {"changes" => {"username" => ["akk", "adam"]}, "metadata" => {}} 38 | end 39 | 40 | def multiple_changes 41 | { 42 | "changes" => {"username" => ["akk", "adam"], "age" => [30, 31]}, 43 | "metadata" => {} 44 | } 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /lib/audit/changeset.rb: -------------------------------------------------------------------------------- 1 | # A structure for tracking individual changes to a record. 2 | Audit::Change = Struct.new(:attribute, :old_value, :new_value) 3 | 4 | # A structure for tracking an atomic group of changes to a model. 5 | class Audit::Changeset < Struct.new(:changes, :metadata) 6 | 7 | # Recreate a changeset given change data as generated by ActiveRecord. 8 | # 9 | # hsh - the Hash to convert to a Changeset. Recognizes two keys: 10 | # "changes" - a Hash of changes as generated by ActiveRecord 11 | # "metadata" - user-provided metadata regarding this change 12 | # 13 | # Examples: 14 | # 15 | # Audit::Changeset.from_hash({"changes" => {'age' => [30, 31]}}) 16 | # # [] 18 | # 19 | # Returns an Array of Changeset objects, one for each changed attribute 20 | def self.from_hash(hsh) 21 | changes = hsh["changes"].map do |k, v| 22 | attribute = k 23 | old_value = v.first 24 | new_value = v.last 25 | Audit::Change.new(attribute, old_value, new_value) 26 | end 27 | new(changes, hsh["metadata"]) 28 | end 29 | 30 | # Recreate a changeset given one or more stored audit records. 31 | # 32 | # enum - an Array of change Hashes (see `from_hash` for details) 33 | # 34 | # Returns an Array of Changeset objects, one for each atomic change 35 | def self.from_enumerable(enum) 36 | case enum 37 | when Hash 38 | from_hash(enum) 39 | when Array 40 | enum.map { |hsh| from_hash(hsh) } 41 | end 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /lib/audit/log.rb: -------------------------------------------------------------------------------- 1 | require 'cassandra/0.7' 2 | require 'active_support/core_ext/module' 3 | require 'simple_uuid' 4 | require 'yajl' 5 | 6 | # Methods for manipulating audit data stored in Cassandra. 7 | module Audit::Log 8 | 9 | # Public: set or fetch the connection to Cassandra that Audit will use. 10 | mattr_accessor :connection 11 | 12 | # Store an audit record. 13 | # 14 | # bucket - the String name for the logical bucket this audit record belongs 15 | # to (i.e. table) 16 | # key - the String key into the logical bucket 17 | # timestamp - timestamp to use for this record's UUID 18 | # changes - the changes hash (as generated by ActiveRecord) to store 19 | # 20 | # Returns nothing. 21 | def self.record(bucket, key, timestamp, changes) 22 | json = Yajl::Encoder.encode(changes) 23 | payload = {timestamp => json} 24 | connection.insert(:Audits, "#{bucket}:#{key}", payload) 25 | end 26 | 27 | # Fetch all audits for a given record. 28 | # 29 | # bucket - the String name for the logical bucket this audit record belongs 30 | # to (i.e. table) 31 | # key - the String key into the logical bucket 32 | # 33 | # Returns an Array of Changeset objects 34 | def self.audits(bucket, key) 35 | # TODO: figure out how to do pagination here 36 | payload = connection.get(:Audits, "#{bucket}:#{key}", :reversed => true) 37 | payload.values.map do |p| 38 | Audit::Changeset.from_enumerable(Yajl::Parser.parse(p)) 39 | end 40 | end 41 | 42 | # Clear all audit data. 43 | # Note that this doesn't yet operate on logical 44 | # buckets. _All_ of the audit data is destroyed. Proceed with caution. 45 | # 46 | # Returns nothing. 47 | def self.clear! 48 | # It'd be nice if this could clear one bucket at a time 49 | connection.clear_keyspace! 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Audit 2 | 3 | Audit sits on top of your model objects and watches for changes to your data. When a change occurs, the differences are recorded and stored in Cassandra. 4 | 5 | ## Usage 6 | 7 | Include `Audit::Tracking` into your change-sensitive ActiveRecord models. When you make changes to data in those tables, the relevant details will be written to a Cassandra column family. 8 | 9 | ## Example 10 | 11 | >> require 'audit' 12 | >> class User < ActiveRecord::Base; include Audit::Tracking; end 13 | >> u = User.create(:name => 'Adam', :city => 'Dallas') 14 | >> u.update_attributes(:city => 'Austin') 15 | >> u.audits 16 | [#]>, #]>, #]>] 17 | 18 | # Compatibility 19 | 20 | Audit is tested against ActiveRecord 3.0, Ruby 1.8.7 and Ruby 1.9.2. 21 | 22 | # Setup 23 | 24 | For Cassandra 0.7, you can set up the schema with `cassandra-cli` like so: 25 | 26 | /* Create a new keyspace */ 27 | create keyspace Audit with replication_factor = 1 28 | 29 | /* Switch to the new keyspace */ 30 | use Audit 31 | 32 | /* Create new column families */ 33 | create column family Audits with column_type = 'Standard' and comparator = 'TimeUUIDType' and rows_cached = 10000 34 | 35 | For Cassandra 0.6, add the following to `storage-conf.xml`: 36 | 37 | 38 | 0.01 39 | 40 | org.apache.cassandra.locator.RackUnawareStrategy 41 | 1 42 | org.apache.cassandra.locator.EndPointSnitch 43 | 44 | 45 | ## Hacking 46 | 47 | Set up RVM: 48 | 49 | $ rvm install ree-1.8.7-2010.01 50 | $ rvm use ree-1.8.7-2010.01 51 | $ rvm gemset create audit 52 | $ rvm gemset use audit 53 | $ gem install bundler 54 | $ bundle install 55 | $ rvm install 1.9.2 56 | $ rvm use 1.9.2 57 | $ rvm gemset create audit 58 | $ rvm gemset use audit 59 | $ gem install bundler 60 | $ bundle install 61 | 62 | Run the test suite with all supported runtimes: 63 | 64 | $ rvm 1.9.2@audit,ree-1.8.7-2010.01@audit rake test 65 | 66 | ## TODO 67 | 68 | - Ignore changes on some attributes 69 | - Add more AR callbacks (delete, ?) 70 | - Generate bucket names for namespaced models 71 | 72 | ## License 73 | 74 | Copyright 2010 Adam Keys `` 75 | 76 | Audit is MIT licensed. Enjoy! 77 | -------------------------------------------------------------------------------- /test/test_tracking.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | ActiveRecord::Base.establish_connection( 4 | :adapter => 'sqlite3', 5 | :database => ':memory:' 6 | ) 7 | 8 | ActiveRecord::Schema.define do 9 | create_table :users do |t| 10 | t.string :username, :null => false 11 | t.integer :age, :null => false 12 | end 13 | end 14 | 15 | class User < ActiveRecord::Base; include Audit::Tracking; end 16 | 17 | class TrackingTest < Test::Unit::TestCase 18 | 19 | def setup 20 | super 21 | @model = User.new 22 | end 23 | 24 | should "generate an audit bucket name based on the model name" do 25 | assert_equal :Users, @model.audit_bucket 26 | end 27 | 28 | should "track audit metadata for the next save" do 29 | audit_metadata = {"reason" => "Canonize usernames", "changed_by" => "JD"} 30 | user = User.create(:username => "adam", :age => 31) 31 | user.audit_metadata(audit_metadata) 32 | user.update_attributes(:username => "therealadam") 33 | changes = user.audits 34 | 35 | assert_equal audit_metadata, changes.first.metadata 36 | 37 | user.save! 38 | 39 | assert_equal({}, user.audit_metadata) # Should clear audit after write 40 | end 41 | 42 | should "add audit-related methods" do 43 | methods = %w{audit audit_bucket audit_metadata audits 44 | skip_audit skip_audit?} 45 | assert_equal methods, @model.methods.map { |s| s.to_s }.grep(/audit/).sort 46 | end 47 | 48 | should "set the log object to an arbitrary object" do 49 | Audit::Tracking.log = flexmock(:log). 50 | should_receive(:audits). 51 | once. 52 | mock 53 | 54 | User.create(:username => "Adam", :age => "31").audits 55 | 56 | Audit::Tracking.log = Audit::Log 57 | end 58 | 59 | should "disable audits for the one write" do 60 | user = User.create(:username => "adam", :age => 31) 61 | user.save! 62 | 63 | user.skip_audit 64 | user.update_attributes(:age => 32) 65 | assert_equal 1, user.audits.length 66 | assert !user.skip_audit? 67 | end 68 | 69 | should "revert the model to the state recorded by an audit" do 70 | user = User.create(:username => "adam", :age => 31) 71 | user.update_attributes(:username => "therealadam") 72 | 73 | user.revert_to(user.audits.first) 74 | 75 | assert_equal "adam", user.username 76 | end 77 | 78 | should "revert the model to the state represented by a series of changes" do 79 | user = User.create(:username => "adam", :age => 31) 80 | user.update_attributes(:username => "therealadam") 81 | user.update_attributes(:age => 32) 82 | user.update_attributes(:username => "akk") 83 | user.update_attributes(:age => 33) 84 | 85 | user.revert(user.audits[0, 4]) 86 | assert_equal 31, user.age 87 | # Heisentest 88 | # assert_equal "adam", user.username 89 | end 90 | 91 | end 92 | -------------------------------------------------------------------------------- /lib/audit/tracking.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/module' 2 | 3 | # Methods for tracking changes to your models by creating audit records 4 | # for every atomic change. Including this module adds callbacks which create 5 | # audit records every time a model object is changed and saved. 6 | module Audit::Tracking 7 | extend ActiveSupport::Concern 8 | 9 | # Public: set the log object to track changes with. 10 | # 11 | # Returns the log object currently in use. 12 | mattr_accessor :log 13 | self.log = Audit::Log 14 | 15 | included do 16 | before_update :audit 17 | end 18 | 19 | # Public: fetch audit records for a model object. 20 | # 21 | # Returns an Array of Changeset objects. 22 | def audits 23 | Audit::Tracking.log.audits(audit_bucket, self.id) 24 | end 25 | 26 | # Creates a new audit record for this model object using data returned by 27 | # ActiveRecord::Base#changes. 28 | # 29 | # Returns nothing. 30 | def audit 31 | if skip_audit? 32 | clear_skip 33 | return 34 | end 35 | 36 | data = {"changes" => changes, "metadata" => audit_metadata} 37 | timestamp = Time.now.utc.iso8601(3) 38 | Audit::Tracking.log.record(audit_bucket, self.id, timestamp, data) 39 | @audit_metadata = {} 40 | end 41 | 42 | # Generates the bucket name for the model class. 43 | # 44 | # Returns a Symbol-ified and pluralized version of the model's name. 45 | def audit_bucket 46 | self.class.name.pluralize.to_sym 47 | end 48 | 49 | # Public: Store audit metadata for the next write. 50 | # 51 | # metadata - a Hash of data that is written alongside the change data 52 | # 53 | # Returns nothing. 54 | def audit_metadata(metadata={}) 55 | @audit_metadata = @audit_metadata.try(:update, metadata) || metadata 56 | end 57 | 58 | # Public: Skip writing audit meatadata for the next write. 59 | # 60 | # Returns: nothing. 61 | def skip_audit 62 | @skip = true 63 | end 64 | 65 | # Public: Write audit metadata for the next write. 66 | # 67 | # Returns: nothing. 68 | def clear_skip 69 | @skip = false 70 | end 71 | 72 | # Public: Flag indicating whether metadata is logged on the next write. 73 | # 74 | # Returns: nothing. 75 | def skip_audit? 76 | @skip ||= false 77 | end 78 | 79 | # Public: Restore the model's attributes to the state recorded by 80 | # a changeset. 81 | # 82 | # changeset - the changes to undo 83 | # 84 | # Returns nothing. 85 | def revert_to(changeset) 86 | changeset.changes.each do |change| 87 | write_attribute(change.attribute, change.old_value) 88 | end 89 | nil 90 | end 91 | 92 | # Public: Restore a model's attributes by reversing a series of changesets. 93 | # 94 | # changesets - the changesets to undo 95 | # 96 | # Returns nothing. 97 | def revert(changesets) 98 | changesets.each do |changeset| 99 | revert_to(changeset) 100 | end 101 | nil 102 | end 103 | 104 | end 105 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | require 'date' 3 | 4 | # Helpers 5 | begin 6 | def name 7 | @name ||= Dir['*.gemspec'].first.split('.').first 8 | end 9 | 10 | def version 11 | line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/] 12 | line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1] 13 | end 14 | 15 | def date 16 | Date.today.to_s 17 | end 18 | 19 | def rubyforge_project 20 | name 21 | end 22 | 23 | def gemspec_file 24 | "#{name}.gemspec" 25 | end 26 | 27 | def gem_file 28 | "#{name}-#{version}.gem" 29 | end 30 | 31 | def replace_header(head, header_name) 32 | head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"} 33 | end 34 | end 35 | 36 | task :default => :test 37 | 38 | Rake::TestTask.new do |t| 39 | t.libs << "test" 40 | end 41 | 42 | desc "Run tests against all supported Ruby versions" 43 | task :compat do 44 | sh "rvm 1.9.2@audit,ree-1.8.7-2010.01@audit rake test" 45 | end 46 | 47 | require 'rake/rdoctask' 48 | Rake::RDocTask.new do |rdoc| 49 | rdoc.rdoc_dir = 'rdoc' 50 | rdoc.title = "#{name} #{version}" 51 | rdoc.rdoc_files.include('README.md') 52 | rdoc.rdoc_files.include('lib/**/*.rb') 53 | end 54 | 55 | desc "Build, tag, push and publish a new release." 56 | task :release => :build do 57 | unless `git branch` =~ /^\* master$/ 58 | puts "You must be on the master branch to release!" 59 | exit! 60 | end 61 | sh "git commit --allow-empty -a -m 'Release #{version}'" 62 | sh "git tag v#{version}" 63 | sh "git push origin master" 64 | sh "git push origin v#{version}" 65 | sh "gem push pkg/#{name}-#{version}.gem" 66 | end 67 | 68 | desc "Build a new gem" 69 | task :build => :gemspec do 70 | sh "mkdir -p pkg" 71 | sh "gem build #{gemspec_file}" 72 | sh "mv #{gem_file} pkg" 73 | end 74 | 75 | desc "Update the generated gemspec" 76 | task :gemspec => :validate do 77 | # read spec file and split out manifest section 78 | spec = File.read(gemspec_file) 79 | head, manifest, tail = spec.split(" # = MANIFEST =\n") 80 | 81 | # replace name version and date 82 | replace_header(head, :name) 83 | replace_header(head, :version) 84 | replace_header(head, :date) 85 | #comment this out if your rubyforge_project has a different name 86 | replace_header(head, :rubyforge_project) 87 | 88 | # determine file list from git ls-files 89 | files = `git ls-files`. 90 | split("\n"). 91 | sort. 92 | reject { |file| file =~ /^\./ }. 93 | reject { |file| file =~ /^(rdoc|pkg)/ }. 94 | map { |file| " #{file}" }. 95 | join("\n") 96 | 97 | # piece file back together and write 98 | manifest = " s.files = %w[\n#{files}\n ]\n" 99 | spec = [head, manifest, tail].join(" # = MANIFEST =\n") 100 | File.open(gemspec_file, 'w') { |io| io.write(spec) } 101 | puts "Updated #{gemspec_file}" 102 | end 103 | 104 | task :validate do 105 | libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"] 106 | unless libfiles.empty? 107 | puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir." 108 | exit! 109 | end 110 | unless Dir['VERSION*'].empty? 111 | puts "A `VERSION` file at root level violates Gem best practices." 112 | exit! 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /audit.gemspec: -------------------------------------------------------------------------------- 1 | ## This is the rakegem gemspec template. Make sure you read and understand 2 | ## all of the comments. Some sections require modification, and others can 3 | ## be deleted if you don't need them. Once you understand the contents of 4 | ## this file, feel free to delete any comments that begin with two hash marks. 5 | ## You can find comprehensive Gem::Specification documentation, at 6 | ## http://docs.rubygems.org/read/chapter/20 7 | Gem::Specification.new do |s| 8 | s.specification_version = 2 if s.respond_to? :specification_version= 9 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 10 | s.rubygems_version = '1.3.5' 11 | 12 | ## Leave these as is they will be modified for you by the rake gemspec task. 13 | ## If your rubyforge_project name is different, then edit it and comment out 14 | ## the sub! line in the Rakefile 15 | s.name = 'audit' 16 | s.version = '0.7.3' 17 | s.date = '2011-10-04' 18 | s.rubyforge_project = 'audit' 19 | 20 | ## Make sure your summary is short. The description may be as long 21 | ## as you like. 22 | s.summary = "Audit logs changes to model objects to Cassandra." 23 | s.description = "Audit sits on top of your model objects and watches for changes to your data. When a change occurs, the differences are recorded and stored in Cassandra." 24 | 25 | ## List the primary authors. If there are a bunch of authors, it's probably 26 | ## better to set the email to an email list or something. If you don't have 27 | ## a custom homepage, consider using your GitHub URL or the like. 28 | s.authors = ["Adam Keys"] 29 | s.email = 'adam@therealadam.com' 30 | s.homepage = 'http://github.com/therealadam/audit' 31 | 32 | ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as 33 | ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb' 34 | s.require_paths = %w[lib] 35 | 36 | ## Specify any RDoc options here. You'll want to add your README and 37 | ## LICENSE files to the extra_rdoc_files list. 38 | s.rdoc_options = ["--charset=UTF-8"] 39 | s.extra_rdoc_files = %w[README.md LICENSE] 40 | 41 | ## List your runtime dependencies here. Runtime dependencies are those 42 | ## that are needed for an end user to actually USE your code. 43 | s.add_dependency('cassandra') 44 | s.add_dependency('yajl-ruby') 45 | 46 | ## List your development dependencies here. Development dependencies are 47 | ## those that are only needed during development 48 | s.add_development_dependency('shoulda', ["~> 2.11.3"]) 49 | s.add_development_dependency('nokogiri', ['~> 1.4.3.1']) 50 | s.add_development_dependency('flexmock', ['~> 0.8.7']) 51 | 52 | ## Leave this section as-is. It will be automatically generated from the 53 | ## contents of your Git repository via the gemspec task. DO NOT REMOVE 54 | ## THE MANIFEST COMMENTS, they are used as delimiters by the task. 55 | # = MANIFEST = 56 | s.files = %w[ 57 | Gemfile 58 | LICENSE 59 | README.md 60 | Rakefile 61 | audit.gemspec 62 | examples/active_model.rb 63 | examples/active_record.rb 64 | examples/auditer.rb 65 | examples/common.rb 66 | lib/audit.rb 67 | lib/audit/changeset.rb 68 | lib/audit/log.rb 69 | lib/audit/tracking.rb 70 | test/ci-build 71 | test/helper.rb 72 | test/storage-conf.xml 73 | test/test_changeset.rb 74 | test/test_log.rb 75 | test/test_tracking.rb 76 | ] 77 | # = MANIFEST = 78 | 79 | ## Test files will be grabbed from the file list. Make sure the path glob 80 | ## matches what you actually use. 81 | s.test_files = s.files.select { |path| path =~ /^test\/.*_test\.rb/ } 82 | end 83 | --------------------------------------------------------------------------------