├── .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 |
--------------------------------------------------------------------------------