diff --git a/.travis.yml b/.travis.yml index 91b2ae6..c241a09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,9 @@ rvm: - jruby-9.0.4.0 - rbx-2.5.8 -services: mongodb +services: + - mongodb + - redis-server gemfile: - gemfiles/rails_3.2_stable.gemfile diff --git a/README.md b/README.md index e704241..6ed77bc 100644 --- a/README.md +++ b/README.md @@ -663,6 +663,21 @@ class Job end ``` +### Redis + +AASM also supports persistence in Redis. +Make sure to include Redis::Objects before you include AASM. + +```ruby +class User + include Redis::Objects + include AASM + + aasm do + end +end +``` + ### Automatic Scopes AASM will automatically create scope methods for each state in the model. diff --git a/gemfiles/rails_4.0.gemfile b/gemfiles/rails_4.0.gemfile index 3209bf9..13fcc50 100644 --- a/gemfiles/rails_4.0.gemfile +++ b/gemfiles/rails_4.0.gemfile @@ -10,5 +10,6 @@ gem 'mongoid', '~>4.0' if Gem::Version.create(RUBY_VERSION.dup) >= Gem::Version. gem 'sequel' gem 'dynamoid', '~> 1', :platforms => :ruby gem 'aws-sdk', '~>2', :platforms => :ruby +gem "redis-objects" gemspec :path => "../" diff --git a/gemfiles/rails_4.1.gemfile b/gemfiles/rails_4.1.gemfile index 1a00497..f407c32 100644 --- a/gemfiles/rails_4.1.gemfile +++ b/gemfiles/rails_4.1.gemfile @@ -10,5 +10,6 @@ gem 'mongoid', '~>4.0' if Gem::Version.create(RUBY_VERSION.dup) >= Gem::Version. gem 'sequel' gem 'dynamoid', '~> 1', :platforms => :ruby gem 'aws-sdk', '~>2', :platforms => :ruby +gem "redis-objects" gemspec :path => "../" diff --git a/gemfiles/rails_4.2_mongo_mapper.gemfile b/gemfiles/rails_4.2_mongo_mapper.gemfile index 8156a55..604a7ee 100644 --- a/gemfiles/rails_4.2_mongo_mapper.gemfile +++ b/gemfiles/rails_4.2_mongo_mapper.gemfile @@ -12,5 +12,6 @@ gem 'mongo_mapper' gem 'bson_ext', :platforms => :ruby gem 'dynamoid', '~> 1', :platforms => :ruby gem 'aws-sdk', '~>2', :platforms => :ruby +gem "redis-objects" gemspec :path => "../" diff --git a/lib/aasm/persistence.rb b/lib/aasm/persistence.rb index 21bfd0a..8adbbef 100644 --- a/lib/aasm/persistence.rb +++ b/lib/aasm/persistence.rb @@ -18,6 +18,8 @@ module AASM include_persistence base, :dynamoid elsif hierarchy.include?("CDQManagedObject") include_persistence base, :core_data_query + elsif hierarchy.include?("Redis::Objects") + include_persistence base, :redis else include_persistence base, :plain end diff --git a/lib/aasm/persistence/redis_persistence.rb b/lib/aasm/persistence/redis_persistence.rb new file mode 100644 index 0000000..bdb0a19 --- /dev/null +++ b/lib/aasm/persistence/redis_persistence.rb @@ -0,0 +1,109 @@ +require_relative "base" + +module AASM + module Persistence + module RedisPersistence + + def self.included(base) + base.send(:include, AASM::Persistence::Base) + base.send(:include, AASM::Persistence::RedisPersistence::InstanceMethods) + end + + module InstanceMethods + # Add the inital value to intiializer + # + # redis-objects removed the key from redis when set to nil + def initialize(*args) + super + state = send(self.class.aasm.attribute_name) + state.value = aasm.determine_state_name(self.class.aasm.initial_state) + end + # Returns the value of the aasm.attribute_name - called from aasm.current_state + # + # If it's a new record, and the aasm state column is blank it returns the initial state + # + # class Foo + # include Redis::Objects + # include AASM + # aasm :column => :status do + # state :opened + # state :closed + # end + # end + # + # foo = Foo.new + # foo.current_state # => :opened + # foo.close + # foo.current_state # => :closed + # + # foo = Foo[1] + # foo.current_state # => :opened + # foo.aasm_state = nil + # foo.current_state # => nil + # + # NOTE: intended to be called from an event + # + # This allows for nil aasm states - be sure to add validation to your model + def aasm_read_state + state = send(self.class.aasm.attribute_name) + + if state.value.nil? + nil + else + state.value.to_sym + end + end + + # Ensures that if the aasm_state column is nil and the record is new + # that the initial state gets populated before validation on create + # + # foo = Foo.new + # foo.aasm_state # => nil + # foo.valid? + # foo.aasm_state # => "open" (where :open is the initial state) + # + # + # foo = Foo.find(:first) + # foo.aasm_state # => 1 + # foo.aasm_state = nil + # foo.valid? + # foo.aasm_state # => nil + # + def aasm_ensure_initial_state + aasm.enter_initial_state if + send(self.class.aasm.attribute_name).to_s.strip.empty? + end + + # Writes state to the state column and persists it to the database + # + # foo = Foo[1] + # foo.aasm.current_state # => :opened + # foo.close! + # foo.aasm.current_state # => :closed + # Foo[1].aasm.current_state # => :closed + # + # NOTE: intended to be called from an event + def aasm_write_state(state) + aasm_column = self.class.aasm.attribute_name + self.send("#{aasm_column}=", state) + end + + # Writes state to the state column, but does not persist it to the database + # + # foo = Foo[1] + # foo.aasm.current_state # => :opened + # foo.close + # foo.aasm.current_state # => :closed + # Foo[1].aasm.current_state # => :opened + # foo.save + # foo.aasm.current_state # => :closed + # Foo[1].aasm.current_state # => :closed + # + # NOTE: intended to be called from an event + def aasm_write_state_without_persistence(state) + send("#{self.class.aasm.attribute_name}=", state) + end + end + end + end +end diff --git a/spec/unit/persistence/redis_persistence_spec.rb b/spec/unit/persistence/redis_persistence_spec.rb new file mode 100644 index 0000000..2448c4d --- /dev/null +++ b/spec/unit/persistence/redis_persistence_spec.rb @@ -0,0 +1,77 @@ + +describe 'redis' do + begin + require 'redis-objects' + require 'logger' + require 'spec_helper' + + before(:all) do + Redis.current = Redis.new(host: '127.0.0.1', port: 6379) + + @model = Class.new do + attr_accessor :default + + include Redis::Objects + include AASM + + value :status + + def id + 1 + end + + aasm column: :status + aasm do + state :alpha, initial: true + state :beta + state :gamma + event :release do + transitions from: [:alpha, :beta, :gamma], to: :beta + end + end + end + end + + describe "instance methods" do + let(:model) {@model.new} + + it "should respond to aasm persistence methods" do + expect(model).to respond_to(:aasm_read_state) + expect(model).to respond_to(:aasm_write_state) + expect(model).to respond_to(:aasm_write_state_without_persistence) + end + + it "should return the initial state when new and the aasm field is nil" do + expect(model.aasm.current_state).to eq(:alpha) + end + + it "should return the aasm column when new and the aasm field is not nil" do + model.status = "beta" + expect(model.aasm.current_state).to eq(:beta) + end + + it "should allow a nil state" do + model.status = nil + expect(model.aasm.current_state).to be_nil + end + end + + describe 'subclasses' do + it "should have the same states as its parent class" do + expect(Class.new(@model).aasm.states).to eq(@model.aasm.states) + end + + it "should have the same events as its parent class" do + expect(Class.new(@model).aasm.events).to eq(@model.aasm.events) + end + + it "should have the same column as its parent even for the new dsl" do + expect(@model.aasm.attribute_name).to eq(:status) + expect(Class.new(@model).aasm.attribute_name).to eq(:status) + end + end + + rescue LoadError + puts "Not running Sequel specs because sequel gem is not installed!!!" + end +end