Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

first commit

  • Loading branch information...
commit 3f37bc767449686c81587f82424058f5d12607f4 0 parents
Mani Tadayon authored October 08, 2010
2  .bundle/config
... ...
@@ -0,0 +1,2 @@
  1
+--- 
  2
+BUNDLE_DISABLE_SHARED_GEMS: "1"
7  .gitignore
... ...
@@ -0,0 +1,7 @@
  1
+tmp
  2
+nbproject
  3
+pkg
  4
+doc/
  5
+html/
  6
+*.swp
  7
+.yardoc
3  Gemfile
... ...
@@ -0,0 +1,3 @@
  1
+source :rubygems
  2
+
  3
+gemspec
72  Gemfile.lock
... ...
@@ -0,0 +1,72 @@
  1
+PATH
  2
+  remote: .
  3
+  specs:
  4
+    workflow_on_mongoid (0.1)
  5
+      mongoid (~> 2.0.0.beta.19)
  6
+      workflow (~> 0.7)
  7
+
  8
+GEM
  9
+  remote: http://rubygems.org/
  10
+  specs:
  11
+    activemodel (3.0.0)
  12
+      activesupport (= 3.0.0)
  13
+      builder (~> 2.1.2)
  14
+      i18n (~> 0.4.1)
  15
+    activerecord (3.0.0)
  16
+      activemodel (= 3.0.0)
  17
+      activesupport (= 3.0.0)
  18
+      arel (~> 1.0.0)
  19
+      tzinfo (~> 0.3.23)
  20
+    activesupport (3.0.0)
  21
+    arel (1.0.1)
  22
+      activesupport (~> 3.0.0)
  23
+    bson (1.0.4)
  24
+    builder (2.1.2)
  25
+    columnize (0.3.1)
  26
+    diff-lcs (1.1.2)
  27
+    i18n (0.4.1)
  28
+    linecache (0.43)
  29
+    mocha (0.9.8)
  30
+      rake
  31
+    mongo (1.0.7)
  32
+      bson (>= 1.0.4)
  33
+    mongoid (2.0.0.beta.18)
  34
+      activemodel (~> 3.0.0)
  35
+      bson (= 1.0.4)
  36
+      mongo (= 1.0.7)
  37
+      tzinfo (~> 0.3.22)
  38
+      will_paginate (~> 3.0.pre)
  39
+    rake (0.8.7)
  40
+    rspec (2.0.0.rc)
  41
+      rspec-core (= 2.0.0.rc)
  42
+      rspec-expectations (= 2.0.0.rc)
  43
+      rspec-mocks (= 2.0.0.rc)
  44
+    rspec-core (2.0.0.rc)
  45
+    rspec-expectations (2.0.0.rc)
  46
+      diff-lcs (>= 1.1.2)
  47
+    rspec-mocks (2.0.0.rc)
  48
+      rspec-core (= 2.0.0.rc)
  49
+      rspec-expectations (= 2.0.0.rc)
  50
+    ruby-debug (0.10.3)
  51
+      columnize (>= 0.1)
  52
+      ruby-debug-base (~> 0.10.3.0)
  53
+    ruby-debug-base (0.10.3)
  54
+      linecache (>= 0.3)
  55
+    sqlite3-ruby (1.3.1)
  56
+    tzinfo (0.3.23)
  57
+    will_paginate (3.0.pre2)
  58
+    workflow (0.7.0)
  59
+
  60
+PLATFORMS
  61
+  ruby
  62
+
  63
+DEPENDENCIES
  64
+  activerecord
  65
+  mocha
  66
+  mongoid (~> 2.0.0.beta.19)
  67
+  rake (= 0.8.7)
  68
+  rspec (~> 2.0.0.rc)
  69
+  ruby-debug
  70
+  sqlite3-ruby
  71
+  workflow (~> 0.7)
  72
+  workflow_on_mongoid!
20  MIT_LICENSE
... ...
@@ -0,0 +1,20 @@
  1
+Copyright (c) 2010 Mani Tadayon
  2
+
  3
+Permission is hereby granted, free of charge, to any person obtaining
  4
+a copy of this software and associated documentation files (the
  5
+"Software"), to deal in the Software without restriction, including
  6
+without limitation the rights to use, copy, modify, merge, publish,
  7
+distribute, sublicense, and/or sell copies of the Software, and to
  8
+permit persons to whom the Software is furnished to do so, subject to
  9
+the following conditions:
  10
+
  11
+The above copyright notice and this permission notice shall be
  12
+included in all copies or substantial portions of the Software.
  13
+
  14
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  15
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  16
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  17
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  18
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  19
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  20
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2  README.md
Source Rendered
... ...
@@ -0,0 +1,2 @@
  1
+workflow_on_mongoid lets you use the [Workflow](http://github.com/geekq/workflow) gem with your Mongoid documents to add state machine functionality.
  2
+-----------------
22  Rakefile
... ...
@@ -0,0 +1,22 @@
  1
+require 'bundler'
  2
+Bundler.setup(:default, :development)
  3
+
  4
+require "rake"
  5
+require "rake/rdoctask"
  6
+require 'rake/testtask'
  7
+
  8
+# require "rspec"
  9
+# require "rspec/core/rake_task"
  10
+
  11
+$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
  12
+require "workflow_on_mongoid/version"
  13
+
  14
+desc 'Default: run all tests.'
  15
+task :default => :test
  16
+
  17
+desc "Test the workflow_on_mongoid plugin."
  18
+Rake::TestTask.new(:test) do |t|
  19
+  t.libs << 'lib'
  20
+  t.test_files = Dir["test/*_test.rb"]
  21
+  t.verbose = true
  22
+end
1  lib/workflow_on_mongoid.rb
... ...
@@ -0,0 +1 @@
  1
+require 'workflow_on_mongoid/workflow'
4  lib/workflow_on_mongoid/version.rb
... ...
@@ -0,0 +1,4 @@
  1
+# encoding: utf-8
  2
+module WorkflowOnMongoid #:nodoc
  3
+  VERSION = "0.1"
  4
+end
33  lib/workflow_on_mongoid/workflow.rb
... ...
@@ -0,0 +1,33 @@
  1
+module Workflow
  2
+  module MongoidInstanceMethods
  3
+    def load_workflow_state
  4
+      send(self.class.workflow_column)
  5
+    end
  6
+
  7
+    def persist_workflow_state(new_value)
  8
+      self.update_attributes!(self.class.workflow_column => new_value)
  9
+    end    
  10
+    
  11
+    private
  12
+      def write_initial_state
  13
+        update_attributes(self.class.workflow_column => current_state.to_s) if load_workflow_state.blank?
  14
+      end    
  15
+  end
  16
+
  17
+  def self.included(klass)
  18
+    klass.send :include, WorkflowInstanceMethods
  19
+    klass.extend WorkflowClassMethods
  20
+    if Object.const_defined?(:ActiveRecord) && klass < ActiveRecord::Base
  21
+      klass.send :include, ActiveRecordInstanceMethods
  22
+      klass.before_validation :write_initial_state
  23
+    elsif Object.const_defined?(:Remodel) && klass < Remodel::Entity
  24
+      klass.send :include, RemodelInstanceMethods
  25
+    elsif Object.const_defined?(:Mongoid) && klass < Mongoid::Document
  26
+      klass.class_eval do
  27
+        field klass.workflow_column
  28
+        include MongoidInstanceMethods
  29
+        klass.before_validation :write_initial_state        
  30
+      end
  31
+    end
  32
+  end  
  33
+end
49  test/couchtiny_example.rb
... ...
@@ -0,0 +1,49 @@
  1
+# require File.join(File.dirname(__FILE__), 'test_helper')
  2
+
  3
+require 'test_helper'
  4
+
  5
+require 'couchtiny'
  6
+require 'couchtiny/document'
  7
+require 'workflow'
  8
+
  9
+class User < CouchTiny::Document
  10
+  include Workflow
  11
+  workflow do
  12
+    state :submitted do
  13
+      event :activate_via_link, :transitions_to => :proved_email
  14
+    end
  15
+    state :proved_email
  16
+  end
  17
+
  18
+  def load_workflow_state
  19
+    self[:workflow_state]
  20
+  end
  21
+
  22
+  def persist_workflow_state(new_value)
  23
+    self[:workflow_state] = new_value
  24
+    save!
  25
+  end
  26
+end
  27
+
  28
+
  29
+class CouchtinyExample < Test::Unit::TestCase
  30
+
  31
+  def setup
  32
+    db = CouchTiny::Database.url("http://127.0.0.1:5984/test-workflow")
  33
+    db.delete_database! rescue nil
  34
+    db.create_database!
  35
+    User.use_database db
  36
+  end
  37
+
  38
+  test 'CouchDB persistence' do
  39
+    user = User.new :email => 'manya@example.com'
  40
+    user.save!
  41
+    assert user.submitted?
  42
+    user.activate_via_link!
  43
+    assert user.proved_email?
  44
+
  45
+    reloaded_user = User.get user.id
  46
+    puts reloaded_user.inspect
  47
+    assert reloaded_user.proved_email?, 'Reloaded user should have the desired workflow state'
  48
+  end
  49
+end
486  test/main_test.rb
... ...
@@ -0,0 +1,486 @@
  1
+$LOAD_PATH.unshift File.expand_path( File.dirname(__FILE__) )
  2
+
  3
+require 'test_helper'
  4
+
  5
+$VERBOSE = false
  6
+require 'active_record/base'
  7
+require 'sqlite3'
  8
+require 'workflow'
  9
+require 'mocha'
  10
+require 'stringio'
  11
+require 'ruby-debug'
  12
+
  13
+ActiveRecord::Migration.verbose = false
  14
+
  15
+class Order < ActiveRecord::Base
  16
+  include Workflow
  17
+  workflow do
  18
+    state :submitted do
  19
+      event :accept, :transitions_to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
  20
+      end
  21
+    end
  22
+    state :accepted do
  23
+      event :ship, :transitions_to => :shipped
  24
+    end
  25
+    state :shipped
  26
+  end
  27
+end
  28
+
  29
+class LegacyOrder < ActiveRecord::Base
  30
+  include Workflow
  31
+
  32
+  workflow_column :foo_bar # use this legacy database column for persistence
  33
+
  34
+  workflow do
  35
+    state :submitted do
  36
+      event :accept, :transitions_to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
  37
+      end
  38
+    end
  39
+    state :accepted do
  40
+      event :ship, :transitions_to => :shipped
  41
+    end
  42
+    state :shipped
  43
+  end
  44
+end
  45
+
  46
+class Image < ActiveRecord::Base
  47
+  include Workflow
  48
+
  49
+  workflow_column :status
  50
+
  51
+  workflow do
  52
+    state :unconverted do
  53
+      event :convert, :transitions_to => :converted
  54
+    end
  55
+    state :converted
  56
+  end
  57
+end
  58
+
  59
+class SmallImage < Image
  60
+end
  61
+
  62
+class SpecialSmallImage < SmallImage
  63
+end
  64
+
  65
+class MainTest < ActiveRecordTestCase
  66
+
  67
+  def setup
  68
+    super
  69
+
  70
+    ActiveRecord::Schema.define do
  71
+      create_table :orders do |t|
  72
+        t.string :title, :null => false
  73
+        t.string :workflow_state
  74
+      end
  75
+    end
  76
+
  77
+    exec "INSERT INTO orders(title, workflow_state) VALUES('some order', 'accepted')"
  78
+
  79
+    ActiveRecord::Schema.define do
  80
+      create_table :legacy_orders do |t|
  81
+        t.string :title, :null => false
  82
+        t.string :foo_bar
  83
+      end
  84
+    end
  85
+
  86
+    exec "INSERT INTO legacy_orders(title, foo_bar) VALUES('some order', 'accepted')"
  87
+
  88
+    ActiveRecord::Schema.define do
  89
+      create_table :images do |t|
  90
+        t.string :title, :null => false
  91
+        t.string :state
  92
+        t.string :type
  93
+      end
  94
+    end
  95
+  end
  96
+
  97
+  def assert_state(title, expected_state, klass = Order)
  98
+    o = klass.find_by_title(title)
  99
+    assert_equal expected_state, o.read_attribute(klass.workflow_column)
  100
+    o
  101
+  end
  102
+
  103
+  test 'immediately save the new workflow_state on state machine transition' do
  104
+    o = assert_state 'some order', 'accepted'
  105
+    assert o.ship!
  106
+    assert_state 'some order', 'shipped'
  107
+  end
  108
+
  109
+  test 'immediately save the new workflow_state on state machine transition with custom column name' do
  110
+    o = assert_state 'some order', 'accepted', LegacyOrder
  111
+    assert o.ship!
  112
+    assert_state 'some order', 'shipped', LegacyOrder
  113
+  end
  114
+
  115
+  test 'persist workflow_state in the db and reload' do
  116
+    o = assert_state 'some order', 'accepted'
  117
+    assert_equal :accepted, o.current_state.name
  118
+    o.ship!
  119
+    o.save!
  120
+
  121
+    assert_state 'some order', 'shipped'
  122
+
  123
+    o.reload
  124
+    assert_equal 'shipped', o.read_attribute(:workflow_state)
  125
+  end
  126
+
  127
+  test 'persist workflow_state in the db with_custom_name and reload' do
  128
+    o = assert_state 'some order', 'accepted', LegacyOrder
  129
+    assert_equal :accepted, o.current_state.name
  130
+    o.ship!
  131
+    o.save!
  132
+
  133
+    assert_state 'some order', 'shipped', LegacyOrder
  134
+
  135
+    o.reload
  136
+    assert_equal 'shipped', o.read_attribute(:foo_bar)
  137
+  end
  138
+
  139
+  test 'default workflow column should be workflow_state' do
  140
+    o = assert_state 'some order', 'accepted'
  141
+    assert_equal :workflow_state, o.class.workflow_column
  142
+  end
  143
+
  144
+  test 'custom workflow column should be foo_bar' do
  145
+    o = assert_state 'some order', 'accepted', LegacyOrder
  146
+    assert_equal :foo_bar, o.class.workflow_column
  147
+  end
  148
+
  149
+  test 'access workflow specification' do
  150
+    assert_equal 3, Order.workflow_spec.states.length
  151
+    assert_equal ['submitted', 'accepted', 'shipped'].sort,
  152
+      Order.workflow_spec.state_names.map{|n| n.to_s}.sort
  153
+  end
  154
+
  155
+  test 'current state object' do
  156
+    o = assert_state 'some order', 'accepted'
  157
+    assert_equal 'accepted', o.current_state.to_s
  158
+    assert_equal 1, o.current_state.events.length
  159
+  end
  160
+
  161
+  test 'on_entry and on_exit invoked' do
  162
+    c = Class.new
  163
+    callbacks = mock()
  164
+    callbacks.expects(:my_on_exit_new).once
  165
+    callbacks.expects(:my_on_entry_old).once
  166
+    c.class_eval do
  167
+      include Workflow
  168
+      workflow do
  169
+        state :new do
  170
+          event :age, :transitions_to => :old
  171
+        end
  172
+        on_exit do
  173
+          callbacks.my_on_exit_new
  174
+        end
  175
+        state :old
  176
+        on_entry do
  177
+          callbacks.my_on_entry_old
  178
+        end
  179
+        on_exit do
  180
+          fail "wrong on_exit executed"
  181
+        end
  182
+      end
  183
+    end
  184
+
  185
+    o = c.new
  186
+    assert_equal 'new', o.current_state.to_s
  187
+    o.age!
  188
+  end
  189
+
  190
+  test 'on_transition invoked' do
  191
+    callbacks = mock()
  192
+    callbacks.expects(:on_tran).once # this is validated at the end
  193
+    c = Class.new
  194
+    c.class_eval do
  195
+      include Workflow
  196
+      workflow do
  197
+        state :one do
  198
+          event :increment, :transitions_to => :two
  199
+        end
  200
+        state :two
  201
+        on_transition do |from, to, triggering_event, *event_args|
  202
+          callbacks.on_tran
  203
+        end
  204
+      end
  205
+    end
  206
+    assert_not_nil c.workflow_spec.on_transition_proc
  207
+    c.new.increment!
  208
+  end
  209
+
  210
+  test 'access event meta information' do
  211
+    c = Class.new
  212
+    c.class_eval do
  213
+      include Workflow
  214
+      workflow do
  215
+        state :main, :meta => {:importance => 8}
  216
+        state :supplemental, :meta => {:importance => 1}
  217
+      end
  218
+    end
  219
+    assert_equal 1, c.workflow_spec.states[:supplemental].meta[:importance]
  220
+  end
  221
+
  222
+  test 'initial state' do
  223
+    c = Class.new
  224
+    c.class_eval do
  225
+      include Workflow
  226
+      workflow { state :one; state :two }
  227
+    end
  228
+    assert_equal 'one', c.new.current_state.to_s
  229
+  end
  230
+
  231
+  test 'nil as initial state' do
  232
+    exec "INSERT INTO orders(title, workflow_state) VALUES('nil state', NULL)"
  233
+    o = Order.find_by_title('nil state')
  234
+    assert o.submitted?, 'if workflow_state is nil, the initial state should be assumed'
  235
+    assert !o.shipped?
  236
+  end
  237
+
  238
+  test 'initial state immediately set as ActiveRecord attribute for new objects' do
  239
+    o = Order.create(:title => 'new object')
  240
+    assert_equal 'submitted', o.read_attribute(:workflow_state)
  241
+  end
  242
+
  243
+  test 'question methods for state' do
  244
+    o = assert_state 'some order', 'accepted'
  245
+    assert o.accepted?
  246
+    assert !o.shipped?
  247
+  end
  248
+
  249
+  test 'correct exception for event, that is not allowed in current state' do
  250
+    o = assert_state 'some order', 'accepted'
  251
+    assert_raise Workflow::NoTransitionAllowed do
  252
+      o.accept!
  253
+    end
  254
+  end
  255
+
  256
+  test 'multiple events with the same name and different arguments lists from different states'
  257
+
  258
+  test 'implicit transition callback' do
  259
+    args = mock()
  260
+    args.expects(:my_tran).once # this is validated at the end
  261
+    c = Class.new
  262
+    c.class_eval do
  263
+      include Workflow
  264
+      def my_transition(args)
  265
+        args.my_tran
  266
+      end
  267
+      workflow do
  268
+        state :one do
  269
+          event :my_transition, :transitions_to => :two
  270
+        end
  271
+        state :two
  272
+      end
  273
+    end
  274
+    c.new.my_transition!(args)
  275
+  end
  276
+
  277
+  test 'Single table inheritance (STI)' do
  278
+    class BigOrder < Order
  279
+    end
  280
+
  281
+    bo = BigOrder.new
  282
+    assert bo.submitted?
  283
+    assert !bo.accepted?
  284
+  end
  285
+
  286
+  test 'STI when parent changed the workflow_state column' do
  287
+    assert_equal 'status', Image.workflow_column.to_s
  288
+    assert_equal 'status', SmallImage.workflow_column.to_s
  289
+    assert_equal 'status', SpecialSmallImage.workflow_column.to_s
  290
+  end
  291
+
  292
+  test 'Two-level inheritance' do
  293
+    class BigOrder < Order
  294
+    end
  295
+
  296
+    class EvenBiggerOrder < BigOrder
  297
+    end
  298
+
  299
+    assert EvenBiggerOrder.new.submitted?
  300
+  end
  301
+
  302
+  test 'Iheritance with workflow definition override' do
  303
+    class BigOrder < Order
  304
+    end
  305
+
  306
+    class SpecialBigOrder < BigOrder
  307
+      workflow do
  308
+        state :start_big
  309
+      end
  310
+    end
  311
+
  312
+    special = SpecialBigOrder.new
  313
+    assert_equal 'start_big', special.current_state.to_s
  314
+  end
  315
+
  316
+  test 'Better error message for missing target state' do
  317
+    class Problem
  318
+      include Workflow
  319
+      workflow do
  320
+        state :initial do
  321
+          event :solve, :transitions_to => :solved
  322
+        end
  323
+      end
  324
+    end
  325
+    assert_raise Workflow::WorkflowError do
  326
+      Problem.new.solve!
  327
+    end
  328
+  end
  329
+
  330
+  # Intermixing of transition graph definition (states, transitions)
  331
+  # on the one side and implementation of the actions on the other side
  332
+  # for a bigger state machine can introduce clutter.
  333
+  #
  334
+  # To reduce this clutter it is now possible to use state entry- and
  335
+  # exit- hooks defined through a naming convention. For example, if there
  336
+  # is a state :pending, then you can hook in by defining method
  337
+  # `def on_pending_exit(new_state, event, *args)` instead of using a
  338
+  # block:
  339
+  #
  340
+  #     state :pending do
  341
+  #       on_entry do
  342
+  #         # your implementation here
  343
+  #       end
  344
+  #     end
  345
+  #
  346
+  # If both a function with a name according to naming convention and the
  347
+  # on_entry/on_exit block are given, then only on_entry/on_exit block is used.
  348
+  test 'on_entry and on_exit hooks in separate methods' do
  349
+    c = Class.new
  350
+    c.class_eval do
  351
+      include Workflow
  352
+      attr_reader :history
  353
+      def initialize
  354
+        @history = []
  355
+      end
  356
+      workflow do
  357
+        state :new do
  358
+          event :next, :transitions_to => :next_state
  359
+        end
  360
+        state :next_state
  361
+      end
  362
+
  363
+      def on_next_state_entry(prior_state, event, *args)
  364
+        @history << "on_next_state_entry #{event} #{prior_state} ->"
  365
+      end
  366
+
  367
+      def on_new_exit(new_state, event, *args)
  368
+        @history << "on_new_exit #{event} -> #{new_state}"
  369
+      end
  370
+    end
  371
+
  372
+    o = c.new
  373
+    assert_equal 'new', o.current_state.to_s
  374
+    assert_equal [], o.history
  375
+    o.next!
  376
+    assert_equal ['on_new_exit next -> next_state', 'on_next_state_entry next new ->'], o.history
  377
+
  378
+  end
  379
+
  380
+  test 'diagram generation' do
  381
+    begin
  382
+      $stdout = StringIO.new('', 'w')
  383
+      Workflow::create_workflow_diagram(Order, 'doc')
  384
+      assert_match(/open.+\.pdf/, $stdout.string,
  385
+        'PDF should be generate and a hint be given to the user.')
  386
+    ensure
  387
+      $stdout = STDOUT
  388
+    end
  389
+  end
  390
+
  391
+  test 'halt stops the transition' do
  392
+    c = Class.new do
  393
+      include Workflow
  394
+      workflow do
  395
+        state :young do
  396
+          event :age, :transitions_to => :old
  397
+        end
  398
+        state :old
  399
+      end
  400
+
  401
+      def age(by=1)
  402
+        halt 'too fast' if by > 100
  403
+      end
  404
+    end
  405
+
  406
+    joe = c.new
  407
+    assert joe.young?
  408
+    joe.age! 120
  409
+    assert joe.young?, 'Transition should have been halted'
  410
+    assert_equal 'too fast', joe.halted_because
  411
+  end
  412
+
  413
+  test 'halt! raises exception immediately' do
  414
+    article_class = Class.new do
  415
+      include Workflow
  416
+      attr_accessor :too_far
  417
+      workflow do
  418
+        state :new do
  419
+          event :reject, :transitions_to => :rejected
  420
+        end
  421
+        state :rejected
  422
+      end
  423
+
  424
+      def reject(reason)
  425
+        halt! 'We do not reject articles unless the reason is important' \
  426
+          unless reason =~ /important/i
  427
+        self.too_far = "This line should not be executed"
  428
+      end
  429
+    end
  430
+
  431
+    article = article_class.new
  432
+    assert article.new?
  433
+    assert_raise Workflow::TransitionHalted do
  434
+      article.reject! 'Too funny'
  435
+    end
  436
+    assert_nil article.too_far
  437
+    assert article.new?, 'Transition should have been halted'
  438
+    article.reject! 'Important: too short'
  439
+    assert article.rejected?, 'Transition should happen now'
  440
+  end
  441
+
  442
+  # published gem doesn't have the `can_?` methods yet!
  443
+  # test 'can fire event?' do
  444
+  #   c = Class.new do
  445
+  #     include Workflow
  446
+  #     workflow do
  447
+  #       state :newborn do
  448
+  #         event :go_to_school, :transitions_to => :schoolboy
  449
+  #       end
  450
+  #       state :schoolboy do
  451
+  #         event :go_to_college, :transitions_to => :student
  452
+  #       end
  453
+  #       state :student
  454
+  #     end
  455
+  #   end
  456
+  # 
  457
+  #   human = c.new
  458
+  #   assert human.can_go_to_school?
  459
+  #   assert_equal false, human.can_go_to_college?
  460
+  # end
  461
+
  462
+  test 'workflow graph generation' do
  463
+    Dir.chdir('tmp') do
  464
+      capture_streams do
  465
+        Workflow::create_workflow_diagram(Order)
  466
+      end
  467
+    end
  468
+  end
  469
+
  470
+  test 'workflow graph generation in path with spaces' do
  471
+    `mkdir -p '/tmp/Workflow test'`
  472
+    capture_streams do
  473
+      Workflow::create_workflow_diagram(Order,  '/tmp/Workflow test')
  474
+    end
  475
+  end
  476
+
  477
+  def capture_streams
  478
+    old_stdout = $stdout
  479
+    $stdout = captured_stdout = StringIO.new
  480
+    yield
  481
+    $stdout = old_stdout
  482
+    captured_stdout
  483
+  end
  484
+
  485
+end
  486
+
463  test/mongoid_test.rb
... ...
@@ -0,0 +1,463 @@
  1
+class MongoidOrder
  2
+  include Mongoid::Document
  3
+  
  4
+  field :title
  5
+  
  6
+  include Workflow
  7
+  workflow do
  8
+    state :submitted do
  9
+      event :accept, :transitions_to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
  10
+      end
  11
+    end
  12
+    state :accepted do
  13
+      event :ship, :transitions_to => :shipped
  14
+    end
  15
+    state :shipped
  16
+  end
  17
+end
  18
+
  19
+class MongoidLegacyOrder
  20
+  include Mongoid::Document  
  21
+  
  22
+  field :title
  23
+  field :foo_bar
  24
+  
  25
+  include Workflow
  26
+
  27
+  workflow_column :foo_bar # use this legacy database column for persistence
  28
+
  29
+  workflow do
  30
+    state :submitted do
  31
+      event :accept, :transitions_to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
  32
+      end
  33
+    end
  34
+    state :accepted do
  35
+      event :ship, :transitions_to => :shipped
  36
+    end
  37
+    state :shipped
  38
+  end
  39
+end
  40
+
  41
+class MongoidImage
  42
+  include Mongoid::Document
  43
+  
  44
+  field :title
  45
+  field :status
  46
+  
  47
+  include Workflow
  48
+
  49
+  workflow_column :status
  50
+
  51
+  workflow do
  52
+    state :unconverted do
  53
+      event :convert, :transitions_to => :converted
  54
+    end
  55
+    state :converted
  56
+  end
  57
+end
  58
+
  59
+class MongoidSmallImage < MongoidImage
  60
+end
  61
+
  62
+class MongoidSpecialSmallImage < MongoidSmallImage
  63
+end
  64
+
  65
+class MongoidTest < MongoidTestCase
  66
+
  67
+  def setup  
  68
+    super
  69
+    
  70
+    MongoidOrder.create!(:title => 'some MongoidOrder', :workflow_state => 'accepted')
  71
+    MongoidLegacyOrder.create!(:title => 'some MongoidOrder', :foo_bar => 'accepted')
  72
+
  73
+  end
  74
+
  75
+  def assert_state(title, expected_state, klass = MongoidOrder)
  76
+    o = klass.where(:title => title).first
  77
+    assert_equal expected_state, o.send(klass.workflow_column)
  78
+    o
  79
+  end
  80
+
  81
+  test 'immediately save the new workflow_state on state machine transition' do
  82
+    o = assert_state 'some MongoidOrder', 'accepted'
  83
+    assert o.ship!
  84
+    assert_state 'some MongoidOrder', 'shipped'
  85
+  end
  86
+
  87
+  test 'immediately save the new workflow_state on state machine transition with custom column name' do
  88
+    o = assert_state 'some MongoidOrder', 'accepted', MongoidLegacyOrder
  89
+    assert o.ship!
  90
+    assert_state 'some MongoidOrder', 'shipped', MongoidLegacyOrder
  91
+  end
  92
+
  93
+  test 'persist workflow_state in the db and reload' do
  94
+    o = assert_state 'some MongoidOrder', 'accepted'
  95
+    assert_equal :accepted, o.current_state.name
  96
+    o.ship!
  97
+    o.save!
  98
+
  99
+    assert_state 'some MongoidOrder', 'shipped'
  100
+
  101
+    o.reload
  102
+    assert_equal 'shipped', o.send(:workflow_state)
  103
+  end
  104
+
  105
+  test 'persist workflow_state in the db with_custom_name and reload' do
  106
+    o = assert_state 'some MongoidOrder', 'accepted', MongoidLegacyOrder
  107
+    assert_equal :accepted, o.current_state.name
  108
+    o.ship!
  109
+    o.save!
  110
+
  111
+    assert_state 'some MongoidOrder', 'shipped', MongoidLegacyOrder
  112
+
  113
+    o.reload
  114
+    assert_equal 'shipped', o.send(:foo_bar)
  115
+  end
  116
+
  117
+  test 'default workflow column should be workflow_state' do
  118
+    o = assert_state 'some MongoidOrder', 'accepted'
  119
+    assert_equal :workflow_state, o.class.workflow_column
  120
+  end
  121
+
  122
+  test 'custom workflow column should be foo_bar' do
  123
+    o = assert_state 'some MongoidOrder', 'accepted', MongoidLegacyOrder
  124
+    assert_equal :foo_bar, o.class.workflow_column
  125
+  end
  126
+
  127
+  test 'access workflow specification' do
  128
+    assert_equal 3, MongoidOrder.workflow_spec.states.length
  129
+    assert_equal ['submitted', 'accepted', 'shipped'].sort,
  130
+      MongoidOrder.workflow_spec.state_names.map{|n| n.to_s}.sort
  131
+  end
  132
+
  133
+  test 'current state object' do
  134
+    o = assert_state 'some MongoidOrder', 'accepted'
  135
+    assert_equal 'accepted', o.current_state.to_s
  136
+    assert_equal 1, o.current_state.events.length
  137
+  end
  138
+
  139
+  test 'on_entry and on_exit invoked' do
  140
+    c = Class.new
  141
+    callbacks = mock()
  142
+    callbacks.expects(:my_on_exit_new).once
  143
+    callbacks.expects(:my_on_entry_old).once
  144
+    c.class_eval do
  145
+      include Workflow
  146
+      workflow do
  147
+        state :new do
  148
+          event :age, :transitions_to => :old
  149
+        end
  150
+        on_exit do
  151
+          callbacks.my_on_exit_new
  152
+        end
  153
+        state :old
  154
+        on_entry do
  155
+          callbacks.my_on_entry_old
  156
+        end
  157
+        on_exit do
  158
+          fail "wrong on_exit executed"
  159
+        end
  160
+      end
  161
+    end
  162
+
  163
+    o = c.new
  164
+    assert_equal 'new', o.current_state.to_s
  165
+    o.age!
  166
+  end
  167
+
  168
+  test 'on_transition invoked' do
  169
+    callbacks = mock()
  170
+    callbacks.expects(:on_tran).once # this is validated at the end
  171
+    c = Class.new
  172
+    c.class_eval do
  173
+      include Workflow
  174
+      workflow do
  175
+        state :one do
  176
+          event :increment, :transitions_to => :two
  177
+        end
  178
+        state :two
  179
+        on_transition do |from, to, triggering_event, *event_args|
  180
+          callbacks.on_tran
  181
+        end
  182
+      end
  183
+    end
  184
+    assert_not_nil c.workflow_spec.on_transition_proc
  185
+    c.new.increment!
  186
+  end
  187
+
  188
+  test 'access event meta information' do
  189
+    c = Class.new
  190
+    c.class_eval do
  191
+      include Workflow
  192
+      workflow do
  193
+        state :main, :meta => {:importance => 8}
  194
+        state :supplemental, :meta => {:importance => 1}
  195
+      end
  196
+    end
  197
+    assert_equal 1, c.workflow_spec.states[:supplemental].meta[:importance]
  198
+  end
  199
+
  200
+  test 'initial state' do
  201
+    c = Class.new
  202
+    c.class_eval do
  203
+      include Workflow
  204
+      workflow { state :one; state :two }
  205
+    end
  206
+    assert_equal 'one', c.new.current_state.to_s
  207
+  end
  208
+
  209
+  test 'nil as initial state' do
  210
+    MongoidOrder.create!(:title => 'nil state', :workflow_state => nil)
  211
+    o = MongoidOrder.where(:title =>'nil state').first
  212
+    assert o.submitted?, 'if workflow_state is nil, the initial state should be assumed'
  213
+    assert !o.shipped?
  214
+  end
  215
+
  216
+  test 'initial state immediately set for new objects' do  
  217
+    o = MongoidOrder.create(:title => 'new object')
  218
+    assert_equal 'submitted', o.send(:workflow_state)
  219
+  end
  220
+
  221
+  test 'question methods for state' do
  222
+    o = assert_state 'some MongoidOrder', 'accepted'
  223
+    assert o.accepted?
  224
+    assert !o.shipped?
  225
+  end
  226
+
  227
+  test 'correct exception for event, that is not allowed in current state' do
  228
+    o = assert_state 'some MongoidOrder', 'accepted'
  229
+    assert_raise Workflow::NoTransitionAllowed do
  230
+      o.accept!
  231
+    end
  232
+  end
  233
+
  234
+  test 'multiple events with the same name and different arguments lists from different states'
  235
+
  236
+  test 'implicit transition callback' do
  237
+    args = mock()
  238
+    args.expects(:my_tran).once # this is validated at the end
  239
+    c = Class.new
  240
+    c.class_eval do
  241
+      include Workflow
  242
+      def my_transition(args)
  243
+        args.my_tran
  244
+      end
  245
+      workflow do
  246
+        state :one do
  247
+          event :my_transition, :transitions_to => :two
  248
+        end
  249
+        state :two
  250
+      end
  251
+    end
  252
+    c.new.my_transition!(args)
  253
+  end
  254
+
  255
+  test 'Single table inheritance (STI)' do
  256
+    class BigMongoidOrder < MongoidOrder
  257
+    end
  258
+
  259
+    bo = BigMongoidOrder.new
  260
+    assert bo.submitted?
  261
+    assert !bo.accepted?
  262
+  end
  263
+
  264
+  test 'STI when parent changed the workflow_state column' do
  265
+    assert_equal 'status', MongoidImage.workflow_column.to_s
  266
+    assert_equal 'status', MongoidSmallImage.workflow_column.to_s
  267
+    assert_equal 'status', MongoidSpecialSmallImage.workflow_column.to_s
  268
+  end
  269
+
  270
+  test 'Two-level inheritance' do
  271
+    class BigMongoidOrder < MongoidOrder
  272
+    end
  273
+
  274
+    class EvenBiggerMongoidOrder < BigMongoidOrder
  275
+    end
  276
+
  277
+    assert EvenBiggerMongoidOrder.new.submitted?
  278
+  end
  279
+
  280
+  test 'Iheritance with workflow definition override' do
  281
+    class BigMongoidOrder < MongoidOrder
  282
+    end
  283
+
  284
+    class SpecialBigMongoidOrder < BigMongoidOrder
  285
+      workflow do
  286
+        state :start_big
  287
+      end
  288
+    end
  289
+
  290
+    special = SpecialBigMongoidOrder.new
  291
+    assert_equal 'start_big', special.current_state.to_s
  292
+  end
  293
+
  294
+  test 'Better error message for missing target state' do
  295
+    class Problem
  296
+      include Workflow
  297
+      workflow do
  298
+        state :initial do
  299
+          event :solve, :transitions_to => :solved
  300
+        end
  301
+      end
  302
+    end
  303
+    assert_raise Workflow::WorkflowError do
  304
+      Problem.new.solve!
  305
+    end
  306
+  end
  307
+
  308
+  # Intermixing of transition graph definition (states, transitions)
  309
+  # on the one side and implementation of the actions on the other side
  310
+  # for a bigger state machine can introduce clutter.
  311
+  #
  312
+  # To reduce this clutter it is now possible to use state entry- and
  313
+  # exit- hooks defined through a naming convention. For example, if there
  314
+  # is a state :pending, then you can hook in by defining method
  315
+  # `def on_pending_exit(new_state, event, *args)` instead of using a
  316
+  # block:
  317
+  #
  318
+  #     state :pending do
  319
+  #       on_entry do
  320
+  #         # your implementation here
  321
+  #       end
  322
+  #     end
  323
+  #
  324
+  # If both a function with a name according to naming convention and the
  325
+  # on_entry/on_exit block are given, then only on_entry/on_exit block is used.
  326
+  test 'on_entry and on_exit hooks in separate methods' do
  327
+    c = Class.new
  328
+    c.class_eval do
  329
+      include Workflow
  330
+      attr_reader :history
  331
+      def initialize
  332
+        @history = []
  333
+      end
  334
+      workflow do
  335
+        state :new do
  336
+          event :next, :transitions_to => :next_state
  337
+        end
  338
+        state :next_state
  339
+      end
  340
+
  341
+      def on_next_state_entry(prior_state, event, *args)
  342
+        @history << "on_next_state_entry #{event} #{prior_state} ->"
  343
+      end
  344
+
  345
+      def on_new_exit(new_state, event, *args)
  346
+        @history << "on_new_exit #{event} -> #{new_state}"
  347
+      end
  348
+    end
  349
+
  350
+    o = c.new
  351
+    assert_equal 'new', o.current_state.to_s
  352
+    assert_equal [], o.history
  353
+    o.next!
  354
+    assert_equal ['on_new_exit next -> next_state', 'on_next_state_entry next new ->'], o.history
  355
+
  356
+  end
  357
+
  358
+  test 'diagram generation' do
  359
+    begin
  360
+      $stdout = StringIO.new('', 'w')
  361
+      Workflow::create_workflow_diagram(MongoidOrder, 'doc')
  362
+      assert_match(/open.+\.pdf/, $stdout.string,
  363
+        'PDF should be generate and a hint be given to the user.')
  364
+    ensure
  365
+      $stdout = STDOUT
  366
+    end
  367
+  end
  368
+
  369
+  test 'halt stops the transition' do
  370
+    c = Class.new do
  371
+      include Workflow
  372
+      workflow do
  373
+        state :young do
  374
+          event :age, :transitions_to => :old
  375
+        end
  376
+        state :old
  377
+      end
  378
+
  379
+      def age(by=1)
  380
+        halt 'too fast' if by > 100
  381
+      end
  382
+    end
  383
+
  384
+    joe = c.new
  385
+    assert joe.young?
  386
+    joe.age! 120
  387
+    assert joe.young?, 'Transition should have been halted'
  388
+    assert_equal 'too fast', joe.halted_because
  389
+  end
  390
+
  391
+  test 'halt! raises exception immediately' do
  392
+    article_class = Class.new do
  393
+      include Workflow
  394
+      attr_accessor :too_far
  395
+      workflow do
  396
+        state :new do
  397
+          event :reject, :transitions_to => :rejected
  398
+        end
  399
+        state :rejected
  400
+      end
  401
+
  402
+      def reject(reason)
  403
+        halt! 'We do not reject articles unless the reason is important' \
  404
+          unless reason =~ /important/i
  405
+        self.too_far = "This line should not be executed"
  406
+      end
  407
+    end
  408
+
  409
+    article = article_class.new
  410
+    assert article.new?
  411
+    assert_raise Workflow::TransitionHalted do
  412
+      article.reject! 'Too funny'
  413
+    end
  414
+    assert_nil article.too_far
  415
+    assert article.new?, 'Transition should have been halted'
  416
+    article.reject! 'Important: too short'
  417
+    assert article.rejected?, 'Transition should happen now'
  418
+  end
  419
+
  420
+  # published gem doesn't have the `can_?` methods yet!
  421
+  # test 'can fire event?' do
  422
+  #   c = Class.new do
  423
+  #     include Workflow
  424
+  #     workflow do
  425
+  #       state :newborn do
  426
+  #         event :go_to_school, :transitions_to => :schoolboy
  427
+  #       end
  428
+  #       state :schoolboy do
  429
+  #         event :go_to_college, :transitions_to => :student
  430
+  #       end
  431
+  #       state :student
  432
+  #     end
  433
+  #   end
  434
+  # 
  435
+  #   human = c.new
  436
+  #   assert human.can_go_to_school?
  437
+  #   assert_equal false, human.can_go_to_college?
  438
+  # end
  439
+
  440
+  test 'workflow graph generation' do
  441
+    Dir.chdir('tmp') do
  442
+      capture_streams do
  443
+        Workflow::create_workflow_diagram(MongoidOrder)
  444
+      end
  445
+    end
  446
+  end
  447
+
  448
+  test 'workflow graph generation in path with spaces' do
  449
+    `mkdir -p '/tmp/Workflow test'`
  450
+    capture_streams do
  451
+      Workflow::create_workflow_diagram(MongoidOrder,  '/tmp/Workflow test')
  452
+    end
  453
+  end
  454
+
  455
+  def capture_streams
  456
+    old_stdout = $stdout
  457
+    $stdout = captured_stdout = StringIO.new