redcrumbs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .DS_Store
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in redcrumbs.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # Redcrumbs
2
+
3
+ Fast and unobtrusive activity tracking of ActiveRecord models using Redis and DataMapper.
4
+
5
+ Redcrumbs is designed for high-traffic applications that need to track changes to their tables without making additional writes to the database. It is especially useful where the saved history needs to be expired over time and is not mission critical data. The emphasis is on reducing response times and easing the load on your main database.
6
+
7
+ User context is built in and fully customisable and this makes Redcrumbs particularly useful for reporting relevant activity to users as it happens in your app.
8
+
9
+ Redcrumbs is used for the 'News' feature in Project Zebra games but could also be used as the basis of a fast versioning or reporting system.
10
+
11
+ For a more complete versioning system see the excellent [vestal_versions](https://github.com/laserlemon/vestal_versions) gem.
12
+
13
+ Please note, this is early stage stuff. We're not using it in production just yet.
14
+
15
+ ## Installation
16
+
17
+ Assuming you've got Redis installed and running on your system just add this to your Gemfile:
18
+
19
+ ```
20
+ gem 'redcrumbs'
21
+ ```
22
+
23
+ Then run the generator to create the initializer file. No migrations necessary!
24
+
25
+ ```
26
+ $ rails g redcrumbs:install
27
+ ```
28
+
29
+ Done! Look in `config/initializers/redcrumbs.rb` for customisation options.
30
+
31
+ ## Example
32
+
33
+ Start tracking a model by adding `redcrumbed` to the class:
34
+
35
+ ```
36
+ class Venue < ActiveRecord::Base
37
+ redcrumbed :only => [:name, :latlng]
38
+
39
+ validates :name, :presence => true
40
+ validates :latlng, :uniqueness => true
41
+ end
42
+ ```
43
+
44
+ And that's pretty much it! Now you can do this:
45
+
46
+ ```
47
+ > venue = Venue.last
48
+ => #<Venue id: 1, name: "Belfast City Hall" ... >
49
+
50
+ > venue.update_attributes(:name => "City Hall, Belfast")
51
+ => #<Venue id: 1, name: "City Hall, Belfast" ... >
52
+
53
+ > venue.crumbs
54
+ => [#<Crumb id: 34 ... >, #<Crumb id: 42 ... >, #<Crumb id: 53 ... >]
55
+
56
+ > crumb = venue.crumbs.last
57
+ => #<Crumb id: 53 ... >
58
+
59
+ > crumb.modifications
60
+ => {"name" => ["Belfast City Hall", "City Hall, Belfast"]}
61
+
62
+ > crumb.subject
63
+ => #<Venue id: 1, name: "City Hall, Belfast" ... >
64
+
65
+ ```
66
+
67
+ Not too shabby. But crumbs can also track the user that made the change (creator), and even a secondary user affected by the change (target). By default the creator is considered to be the user associated with the object:
68
+
69
+ ```
70
+ > user = User.find(2)
71
+ => #<User id: 2, name: "Jon" ... >
72
+
73
+ > venue = user.venues.last
74
+ => #<Venue id: 1, name: "City Hall, Belfast", user_id: 2 ... >
75
+
76
+ > venue.update_attributes(:name => "Halla na Cathrach, Bhéal Feirste")
77
+ => #<Venue id: 1, name: "Halla na Cathrach, Bhéal Feirste", user_id: 2 ... >
78
+
79
+ > crumb = venue.crumbs.last
80
+ => #<Crumb id: 54 ... >
81
+
82
+ > crumb.modifications
83
+ => {"name" => ["City Hall, Belfast", "Halla na Cathrach, Bhéal Feirste"]}
84
+
85
+ > crumb.creator
86
+ => #<User id: 2, name: "Jon" ... >
87
+
88
+ # and really cool, returns a limited (default 100) array of crumbs affecting a user in reverse order:
89
+ > user.crumbs(:limit => 20)
90
+ => [#<Crumb id: 64 ... >, #<Crumb id: 53 ... >, #<Crumb id: 42 ... > ... ]
91
+
92
+ # or if you just want the crumbs created by the user
93
+ > user.crumbs_by
94
+
95
+ # or affecting the user
96
+ > user.crumbs_for
97
+
98
+ ```
99
+
100
+ You can customise just what should be considered a creator or target globally across your app by editing a few lines in the redcrumbs initializer. Or you can override the creator and target methods if you want class-specific control:
101
+
102
+ ```
103
+ class User < ActiveRecord::Base
104
+ belongs_to :alliance
105
+ has_many :venues
106
+ end
107
+
108
+ class Venue < ActiveRecord::Base
109
+ redcrumbed :only => [:name, :latlng]
110
+
111
+ belongs_to :user
112
+
113
+ validates :name, :presence => true
114
+ validates :latlng, :uniqueness => true
115
+
116
+ def creator
117
+ user.alliance
118
+ end
119
+ end
120
+ ```
121
+
122
+ ## Conditional control
123
+
124
+ You can pass `:if` and `:unless` options to the redcrumbed method to control when an action should be tracked in the same way you would for an ActiveRecord callback. For example:
125
+
126
+ ```
127
+ class Venue < ActiveRecord::Base
128
+ redcrumbed :only => [:name, :latlng], :if => :has_user?
129
+
130
+ def has_user?
131
+ !!user_id
132
+ end
133
+ end
134
+ ```
135
+
136
+ ## Attribute storage
137
+
138
+ It's not best practice but since the emphasis is on easing the load on our main database we have bent a few rules in order to reduce the calls on the database to, ideally, zero. In any given app you may be tracking several models and this results in a lot of SQL we could do without.
139
+
140
+ `redcrumbed` accepts a `:store` option to which you can pass an array of attributes of the subject that you'd like to store on the crumb object itself. Use it sparingly if you know that, for example, you are only ever going to really use a couple of attributes of the subject and you want to avoid loading the whole thing from the database.
141
+
142
+ ```
143
+ class Venue
144
+ redcrumbed :only => [:name, :latlng], :store => [:id, :name]
145
+ end
146
+ ```
147
+
148
+ So now if you call `crumb.subject` instead of loading the Venue from your database it will instantiate a new Venue with the same `id` and `name` attributes. You can always retrieve the original by calling `crumb.full_subject`.
149
+
150
+ As you might expect, you can also do this for the creator and target of the crumb. See the redcrumbs.rb initializer for how to set this as a global configuration.
151
+
152
+
153
+ ## To-do
154
+
155
+ Lots of refactoring, tests and new features.
156
+
157
+ ## License
158
+
159
+ Created by John Hope (c) 2012 for Project Zebra. Released under MIT License (http://www.opensource.org/licenses/mit-license.php).
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,23 @@
1
+ module Redcrumbs
2
+ module Crumb::Expiry
3
+ extend ActiveSupport::Concern
4
+
5
+ def mortal?
6
+ !!Redcrumbs.mortality
7
+ end
8
+
9
+ def expire_at
10
+ Time.now + Redcrumbs.mortality
11
+ end
12
+
13
+ def time_to_live
14
+ REDIS.ttl(redis_key) if mortal?
15
+ end
16
+
17
+ private
18
+
19
+ def set_mortality
20
+ REDIS.expireat(redis_key, expire_at.to_i) if mortal?
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ module Redcrumbs
2
+ module Crumb::Getters
3
+ extend ActiveSupport::Concern
4
+
5
+ def subject
6
+ if !self.stored_subject.blank?
7
+ subject_from_storage
8
+ elsif subject_type && subject_id
9
+ self._subject ||= full_subject
10
+ end
11
+ end
12
+
13
+ def full_subject
14
+ subject_type.classify.constantize.find(subject_id)
15
+ end
16
+
17
+ def subject_from_storage
18
+ new_subject = subject_type.constantize.new(self.stored_subject)
19
+ new_subject.id = self.stored_subject["id"] if self.stored_subject.has_key?("id")
20
+ new_subject
21
+ end
22
+
23
+ def creator
24
+ if !self.stored_creator.blank?
25
+ creator_class.new(self.stored_creator)
26
+ elsif !self.creator_id.blank?
27
+ self._creator ||= full_creator
28
+ end
29
+ end
30
+
31
+ def creator_class
32
+ Redcrumbs.creator_class_sym.to_s.classify.constantize
33
+ end
34
+
35
+ def full_creator
36
+ creator_class.where(Redcrumbs.creator_primary_key => self.creator_id).first
37
+ end
38
+
39
+ def target
40
+ if !self.stored_target.blank?
41
+ target_class.new(self.stored_target)
42
+ elsif !self.target_id.blank?
43
+ self._target ||= full_target
44
+ end
45
+ end
46
+
47
+ def target_class
48
+ Redcrumbs.target_class_sym.to_s.classify.constantize
49
+ end
50
+
51
+ def full_target
52
+ target_class.where(Redcrumbs.target_primary_key => self.creator_id).first
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,25 @@
1
+ module Redcrumbs
2
+ module Crumb::Setters
3
+ extend ActiveSupport::Concern
4
+
5
+ def subject=(subject)
6
+ self.stored_subject = subject.storeable_attributes
7
+ self.subject_type = subject.class.to_s
8
+ self.subject_id = subject.id
9
+ end
10
+
11
+ def creator=(creator)
12
+ unless !creator
13
+ self.stored_creator = creator.attributes.select {|attribute| Redcrumbs.store_creator_attributes.include?(attribute.to_sym)}
14
+ self.creator_id = creator[Redcrumbs.creator_primary_key]
15
+ end
16
+ end
17
+
18
+ def target=(target)
19
+ unless !target
20
+ self.stored_target = target.attributes.select {|attribute| Redcrumbs.store_target_attributes.include?(attribute.to_sym)}
21
+ self.target_id = target[Redcrumbs.target_primary_key]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,64 @@
1
+ require 'dm-core'
2
+ require 'dm-types'
3
+ require 'dm-timestamps'
4
+ require 'dm-redis-adapter'
5
+
6
+ ## Note to self: Syntax to grab all by an attribute is - Notification.all(:subject_type => "User")
7
+ ## The attribute must be indexed as with subject_type below
8
+
9
+ module Redcrumbs
10
+ class Crumb
11
+ REDIS = Redis.new
12
+
13
+ include DataMapper::Resource
14
+ include Crumb::Getters
15
+ include Crumb::Setters
16
+ include Crumb::Expiry
17
+
18
+ DataMapper.setup(:default, {:adapter => "redis"})
19
+
20
+ property :id, Serial
21
+ property :subject_id, Integer, :index => true
22
+ property :subject_type, String, :index => true
23
+ property :modifications, Json, :default => "{}"
24
+ property :created_at, DateTime
25
+ property :updated_at, DateTime
26
+ property :stored_creator, Json
27
+ property :stored_target, Json
28
+ property :stored_subject, Json
29
+ property :creator_id, Integer, :index => true
30
+ property :target_id, Integer, :index => true
31
+
32
+ DataMapper.finalize
33
+
34
+ after :save, :set_mortality
35
+
36
+ attr_accessor :_subject, :_creator, :_target
37
+
38
+ def initialize(params = {})
39
+ if self.subject = params[:subject]
40
+ self.target = self.subject.target if self.subject.respond_to?(:target)
41
+ self.creator = self.subject.creator if self.subject.respond_to?(:creator)
42
+ end
43
+ self.modifications = params[:modifications] unless !params[:modifications]
44
+ end
45
+
46
+ # Remember to change the respond_to? argument when moving from user/target class to dynamic with user as default
47
+ def self.build_from(subject)
48
+ unless subject.watched_changes.empty?
49
+ params = {:modifications => subject.watched_changes}
50
+ params.merge!({:subject => subject})
51
+ new(params)
52
+ end
53
+ end
54
+
55
+ def redis_key
56
+ "redcrumbs_crumbs:#{id}"
57
+ end
58
+
59
+ # Designed to mimic ActiveRecord's count. Probably not performant and only should be used for tests really
60
+ def self.count
61
+ REDIS.keys("redcrumbs_crumbs:*").size - 8
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,12 @@
1
+ module Redcrumbs
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../templates", __FILE__)
5
+
6
+ # all public methods in here will be run in order
7
+ def add_redcrumbs_initializer
8
+ template "initializer.rb", "config/initializers/redcrumbs.rb"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ Redcrumbs.setup do |config|
2
+ # When a change is made to a redcrumbed model the user associated with that model will
3
+ # automatically be stored on the Crumb object. By default Redcrumbs will look for a User
4
+ # object using 'id' as the primary key but you can override that here.
5
+ #
6
+ # config.creator_class_sym = :user
7
+ # config.creator_primary_key = 'id'
8
+ # config.target_class_sym = :user
9
+ # config.target_primary_key = 'id'
10
+ #
11
+ #
12
+ # If you're using the crumbs to report news back to a user you can store creator and target
13
+ # attributes on the crumb object to avoid having to touch your main database at all. Keep it
14
+ # sensible and evaluate whether the additional space used in Redis is really worth the time saving.
15
+ #
16
+ # config.store_creator_attributes = [:id, :name, :email]
17
+ #
18
+ #
19
+ # Set the mortality to make crumbs automatically expire in time. Default is infinity.
20
+ # config.mortality = 30.days
21
+ end
@@ -0,0 +1,19 @@
1
+ module Redcrumbs
2
+ mattr_accessor :creator_class_sym
3
+ mattr_accessor :creator_primary_key
4
+ mattr_accessor :target_class_sym
5
+ mattr_accessor :target_primary_key
6
+
7
+ mattr_accessor :store_creator_attributes
8
+ mattr_accessor :store_target_attributes
9
+
10
+ mattr_accessor :mortality
11
+
12
+ @@creator_class_sym ||= :user
13
+ @@creator_primary_key ||= 'id'
14
+ @@target_class_sym ||= :user
15
+ @@target_primary_key ||= 'id'
16
+
17
+ @@store_creator_attributes ||= []
18
+ @@store_target_attributes ||= []
19
+ end
@@ -0,0 +1,31 @@
1
+ module Redcrumbs
2
+ module Creation
3
+ extend ActiveSupport::Concern
4
+
5
+ module InstanceMethods
6
+ def crumbs
7
+ Crumb.all(:subject_type => self.class.to_s, :subject_id => self.id)
8
+ end
9
+
10
+ def watched_changes
11
+ changes.reject {|k,v| !self.class.redcrumbs_options[:only].include?(k.to_sym)}
12
+ end
13
+
14
+ def storeable_attributes
15
+ attributes.reject {|k,v| !self.class.redcrumbs_options[:store].include?(k.to_sym)}
16
+ end
17
+
18
+ def watched_changes_empty?
19
+ watched_changes.empty?
20
+ end
21
+
22
+ def notify_changes
23
+ unless watched_changes.empty?
24
+ n = Crumb.build_from(self)
25
+ n.save
26
+ end
27
+ yield
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,8 @@
1
+ require "rails/engine"
2
+ require "redcrumbs"
3
+
4
+ module Redcrumbs
5
+ class Engine < Rails::Engine
6
+ # Nothing to see here. Consider moving the Crumb model from engine into lib in the future.
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ module Redcrumbs
2
+ module Options
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # prepare_redcrumbed_options prepares class level options that customise the behaviour of
7
+ # redcrumbed. See documentation for a full explanation of redcrumbed options.
8
+ def prepare_redcrumbed_options(options)
9
+ defaults = {
10
+ :only => [],
11
+ :store => []
12
+ }
13
+
14
+ options.reverse_merge!(defaults)
15
+
16
+ options[:only] = Array(options[:only])
17
+ options[:store] = Array(options[:store])
18
+
19
+ class_inheritable_accessor :redcrumbs_options
20
+ class_inheritable_accessor :redcrumbs_callback_options
21
+
22
+ self.redcrumbs_options = options.dup
23
+ self.redcrumbs_callback_options = options.dup.select {|k,v| [:if, :unless].include?(k.to_sym)}
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,35 @@
1
+ module Redcrumbs
2
+ # Provides methods for giving user context to crumbs. Retrieves crumbs created by a user (creator) or
3
+ # affecting a user (target)
4
+ module Users
5
+ extend ActiveSupport::Concern
6
+
7
+ module InstanceMethods
8
+ # Retrieves crumbs related to the user
9
+ def crumbs_for
10
+ Crumb.all(:target_id => self[Redcrumbs.target_primary_key], :order => [:created_at.desc])
11
+ end
12
+
13
+ # Retrieves crumbs created by the user
14
+ def crumbs_by
15
+ Crumb.all(:creator_id => self[Redcrumbs.creator_primary_key], :order => [:created_at.desc])
16
+ end
17
+
18
+ # A limitable collection of both crumbs_for and crumbs_by
19
+ # This is an unforunate hack to get over the redis dm adapter's non-support of addition (OR) queries
20
+ def crumbs_as_user(opts = {})
21
+ opts[:limit] ||= 100
22
+ arr = crumbs_for
23
+ arr += crumbs_by
24
+ arr.all(:limit => opts[:limit])
25
+ end
26
+
27
+ # Creator method defines who should be considered the creator when a model is updated. This
28
+ # can be overridden in the redcrumbed model to define who the creator should be. Defaults
29
+ # to the current user (or creator class) associated with the model.
30
+ def creator
31
+ send(Redcrumbs.creator_class_sym) if respond_to?(Redcrumbs.creator_class_sym)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module Redcrumbs
2
+ VERSION = "0.1.0"
3
+ end
data/lib/redcrumbs.rb ADDED
@@ -0,0 +1,63 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/dependencies/autoload'
3
+ require "redcrumbs/version"
4
+ require 'redcrumbs/engine'
5
+ require 'dm-core'
6
+
7
+ # Redcrumbs implements dirty models to track changes to ActiveRecord models in a way that is fast and
8
+ # unobtrusive. By storing the data in Redis instead of a SQL database the footprint is greatly reduced, no
9
+ # schema changes are necessary and we can harness all the advantages of a key value store; such as key expiry.
10
+ #
11
+ # Author:: John Hope
12
+ # Copyright:: Copyright (c) 2012 John Hope for Project Zebra
13
+ # License:: MIT License (http://www.opensource.org/licenses/mit-license.php)
14
+ #
15
+ # To start tracking a model use the 'redcrumbed' method:
16
+ #
17
+ # class Venue
18
+ # redcrumbed, :only => [:name, :latlng]
19
+ # end
20
+ #
21
+ # venue = Venue.last
22
+ # venue.crumbs(:limit => 20)
23
+ # crumb = venue.crumbs.last
24
+ # crumb.creator
25
+ # => #<User ... >
26
+ # crumb.modifications
27
+ # => {"name"=>["Belfast City Hall", "The City Hall, Belfast"]}
28
+ #
29
+ # See the documentation for more details on how to customise and use Redcrumbs.
30
+
31
+ module Redcrumbs
32
+ extend ActiveSupport::Concern
33
+ extend ActiveSupport::Autoload
34
+
35
+ autoload :Options
36
+ autoload :Config
37
+ autoload :Users
38
+ autoload :Creation
39
+
40
+ include Config
41
+
42
+ def self.setup
43
+ yield self
44
+ end
45
+
46
+ def self.included(base)
47
+ base.extend(ClassMethods)
48
+ end
49
+
50
+ module ClassMethods
51
+ def redcrumbed(options = {})
52
+ include Options
53
+ include Users
54
+ include Creation
55
+
56
+ prepare_redcrumbed_options(options)
57
+
58
+ around_save :notify_changes, self.redcrumbs_callback_options
59
+ end
60
+ end
61
+ end
62
+
63
+ ActiveRecord::Base.class_eval { include Redcrumbs }
data/redcrumbs.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "redcrumbs/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "redcrumbs"
7
+ s.version = Redcrumbs::VERSION
8
+ s.authors = ["John Hope"]
9
+ s.email = ["info@midhirrecords.com"]
10
+ s.homepage = "https://github.com/projectzebra/Redcrumbs"
11
+ s.summary = %q{Fast and unobtrusive activity tracking of ActiveRecord models using DataMapper and Redis}
12
+ s.description = %q{Fast and unobtrusive activity tracking of ActiveRecord models using DataMapper and Redis}
13
+
14
+ s.rubyforge_project = "redcrumbs"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency 'data_mapper', '>= 1.2.0'
22
+ s.add_dependency 'dm-redis-adapter', '>= 0.6.2'
23
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redcrumbs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - John Hope
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-21 00:00:00.000000000 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: data_mapper
17
+ requirement: &2152286280 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 1.2.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *2152286280
26
+ - !ruby/object:Gem::Dependency
27
+ name: dm-redis-adapter
28
+ requirement: &2152285780 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.6.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *2152285780
37
+ description: Fast and unobtrusive activity tracking of ActiveRecord models using DataMapper
38
+ and Redis
39
+ email:
40
+ - info@midhirrecords.com
41
+ executables: []
42
+ extensions: []
43
+ extra_rdoc_files: []
44
+ files:
45
+ - .gitignore
46
+ - Gemfile
47
+ - README.md
48
+ - Rakefile
49
+ - app/models/redcrumbs/crumb.rb
50
+ - app/models/redcrumbs/crumb/expiry.rb
51
+ - app/models/redcrumbs/crumb/getters.rb
52
+ - app/models/redcrumbs/crumb/setters.rb
53
+ - lib/generators/redcrumbs/install_generator.rb
54
+ - lib/generators/redcrumbs/templates/initializer.rb
55
+ - lib/redcrumbs.rb
56
+ - lib/redcrumbs/config.rb
57
+ - lib/redcrumbs/creation.rb
58
+ - lib/redcrumbs/engine.rb
59
+ - lib/redcrumbs/options.rb
60
+ - lib/redcrumbs/users.rb
61
+ - lib/redcrumbs/version.rb
62
+ - redcrumbs.gemspec
63
+ has_rdoc: true
64
+ homepage: https://github.com/projectzebra/Redcrumbs
65
+ licenses: []
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubyforge_project: redcrumbs
84
+ rubygems_version: 1.6.2
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: Fast and unobtrusive activity tracking of ActiveRecord models using DataMapper
88
+ and Redis
89
+ test_files: []