telvue_state_machine 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +72 -0
- data/.yardopts +5 -0
- data/Appraisals +491 -0
- data/CHANGELOG.md +502 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +1263 -0
- data/Rakefile +41 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/Gemfile +5 -0
- data/examples/Gemfile.lock +14 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +13 -0
- data/examples/car.rb +21 -0
- data/examples/doc/AutoShop.html +2856 -0
- data/examples/doc/AutoShop_state.png +0 -0
- data/examples/doc/Car.html +919 -0
- data/examples/doc/Car_state.png +0 -0
- data/examples/doc/TrafficLight.html +2230 -0
- data/examples/doc/TrafficLight_state.png +0 -0
- data/examples/doc/Vehicle.html +7921 -0
- data/examples/doc/Vehicle_state.png +0 -0
- data/examples/doc/_index.html +136 -0
- data/examples/doc/class_list.html +47 -0
- data/examples/doc/css/common.css +1 -0
- data/examples/doc/css/full_list.css +55 -0
- data/examples/doc/css/style.css +322 -0
- data/examples/doc/file_list.html +46 -0
- data/examples/doc/frames.html +13 -0
- data/examples/doc/index.html +136 -0
- data/examples/doc/js/app.js +205 -0
- data/examples/doc/js/full_list.js +173 -0
- data/examples/doc/js/jquery.js +16 -0
- data/examples/doc/method_list.html +734 -0
- data/examples/doc/top-level-namespace.html +105 -0
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +7 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view__form.html.erb +34 -0
- data/examples/rails-rest/view_edit.html.erb +6 -0
- data/examples/rails-rest/view_index.html.erb +25 -0
- data/examples/rails-rest/view_new.html.erb +5 -0
- data/examples/rails-rest/view_show.html.erb +19 -0
- data/examples/traffic_light.rb +9 -0
- data/examples/vehicle.rb +33 -0
- data/gemfiles/active_model_3.0.0.gemfile +7 -0
- data/gemfiles/active_model_3.0.0.gemfile.lock +35 -0
- data/gemfiles/active_model_3.0.5.gemfile +7 -0
- data/gemfiles/active_model_3.0.5.gemfile.lock +35 -0
- data/gemfiles/active_model_3.1.1.gemfile +7 -0
- data/gemfiles/active_model_3.1.1.gemfile.lock +36 -0
- data/gemfiles/active_model_3.2.1.gemfile +7 -0
- data/gemfiles/active_model_3.2.12.gemfile +7 -0
- data/gemfiles/active_model_3.2.12.gemfile.lock +36 -0
- data/gemfiles/active_model_3.2.13.rc1.gemfile +7 -0
- data/gemfiles/active_model_3.2.13.rc1.gemfile.lock +36 -0
- data/gemfiles/active_model_4.0.0.gemfile +9 -0
- data/gemfiles/active_model_4.0.0.gemfile.lock +78 -0
- data/gemfiles/active_record_2.0.0.gemfile +9 -0
- data/gemfiles/active_record_2.0.0.gemfile.lock +39 -0
- data/gemfiles/active_record_2.0.5.gemfile +9 -0
- data/gemfiles/active_record_2.0.5.gemfile.lock +39 -0
- data/gemfiles/active_record_2.1.0.gemfile +9 -0
- data/gemfiles/active_record_2.1.0.gemfile.lock +39 -0
- data/gemfiles/active_record_2.1.2.gemfile +9 -0
- data/gemfiles/active_record_2.1.2.gemfile.lock +39 -0
- data/gemfiles/active_record_2.2.3.gemfile +9 -0
- data/gemfiles/active_record_2.2.3.gemfile.lock +39 -0
- data/gemfiles/active_record_2.3.12.gemfile +9 -0
- data/gemfiles/active_record_2.3.12.gemfile.lock +39 -0
- data/gemfiles/active_record_2.3.5.gemfile +9 -0
- data/gemfiles/active_record_2.3.5.gemfile.lock +39 -0
- data/gemfiles/active_record_3.0.0.gemfile +9 -0
- data/gemfiles/active_record_3.0.0.gemfile.lock +51 -0
- data/gemfiles/active_record_3.0.5.gemfile +9 -0
- data/gemfiles/active_record_3.0.5.gemfile.lock +50 -0
- data/gemfiles/active_record_3.1.1.gemfile +9 -0
- data/gemfiles/active_record_3.1.1.gemfile.lock +51 -0
- data/gemfiles/active_record_3.2.12.gemfile +9 -0
- data/gemfiles/active_record_3.2.12.gemfile.lock +51 -0
- data/gemfiles/active_record_3.2.13.rc1.gemfile +9 -0
- data/gemfiles/active_record_3.2.13.rc1.gemfile.lock +51 -0
- data/gemfiles/active_record_4.0.0.gemfile +11 -0
- data/gemfiles/active_record_4.0.0.gemfile.lock +83 -0
- data/gemfiles/data_mapper_0.10.2.gemfile +13 -0
- data/gemfiles/data_mapper_0.10.2.gemfile.lock +56 -0
- data/gemfiles/data_mapper_0.9.11.gemfile +13 -0
- data/gemfiles/data_mapper_0.9.11.gemfile.lock +71 -0
- data/gemfiles/data_mapper_0.9.4.gemfile +12 -0
- data/gemfiles/data_mapper_0.9.4.gemfile.lock +70 -0
- data/gemfiles/data_mapper_0.9.7.gemfile +13 -0
- data/gemfiles/data_mapper_0.9.7.gemfile.lock +67 -0
- data/gemfiles/data_mapper_1.0.0.gemfile +12 -0
- data/gemfiles/data_mapper_1.0.0.gemfile.lock +63 -0
- data/gemfiles/data_mapper_1.0.1.gemfile +12 -0
- data/gemfiles/data_mapper_1.0.1.gemfile.lock +63 -0
- data/gemfiles/data_mapper_1.0.2.gemfile +12 -0
- data/gemfiles/data_mapper_1.0.2.gemfile.lock +63 -0
- data/gemfiles/data_mapper_1.1.0.gemfile +12 -0
- data/gemfiles/data_mapper_1.1.0.gemfile.lock +61 -0
- data/gemfiles/data_mapper_1.2.0.gemfile +12 -0
- data/gemfiles/data_mapper_1.2.0.gemfile.lock +61 -0
- data/gemfiles/default.gemfile +7 -0
- data/gemfiles/default.gemfile.lock +27 -0
- data/gemfiles/graphviz_0.9.17.gemfile +7 -0
- data/gemfiles/graphviz_0.9.17.gemfile.lock +29 -0
- data/gemfiles/graphviz_0.9.21.gemfile +7 -0
- data/gemfiles/graphviz_0.9.21.gemfile.lock +29 -0
- data/gemfiles/graphviz_1.0.0.gemfile +7 -0
- data/gemfiles/graphviz_1.0.0.gemfile.lock +29 -0
- data/gemfiles/graphviz_1.0.3.gemfile +7 -0
- data/gemfiles/graphviz_1.0.3.gemfile.lock +29 -0
- data/gemfiles/graphviz_1.0.8.gemfile +7 -0
- data/gemfiles/graphviz_1.0.8.gemfile.lock +29 -0
- data/gemfiles/mongo_mapper_0.10.0.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.10.0.gemfile.lock +47 -0
- data/gemfiles/mongo_mapper_0.11.2.gemfile +9 -0
- data/gemfiles/mongo_mapper_0.11.2.gemfile.lock +48 -0
- data/gemfiles/mongo_mapper_0.12.0.gemfile +9 -0
- data/gemfiles/mongo_mapper_0.12.0.gemfile.lock +48 -0
- data/gemfiles/mongo_mapper_0.5.5.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.5.5.gemfile.lock +36 -0
- data/gemfiles/mongo_mapper_0.5.8.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.5.8.gemfile.lock +36 -0
- data/gemfiles/mongo_mapper_0.6.0.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.6.0.gemfile.lock +36 -0
- data/gemfiles/mongo_mapper_0.6.10.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.6.10.gemfile.lock +36 -0
- data/gemfiles/mongo_mapper_0.7.0.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.7.0.gemfile.lock +36 -0
- data/gemfiles/mongo_mapper_0.7.5.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.7.5.gemfile.lock +39 -0
- data/gemfiles/mongo_mapper_0.8.0.gemfile +10 -0
- data/gemfiles/mongo_mapper_0.8.0.gemfile.lock +43 -0
- data/gemfiles/mongo_mapper_0.8.3.gemfile +10 -0
- data/gemfiles/mongo_mapper_0.8.3.gemfile.lock +43 -0
- data/gemfiles/mongo_mapper_0.8.4.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.8.4.gemfile.lock +42 -0
- data/gemfiles/mongo_mapper_0.8.6.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.8.6.gemfile.lock +42 -0
- data/gemfiles/mongo_mapper_0.9.0.gemfile +7 -0
- data/gemfiles/mongo_mapper_0.9.0.gemfile.lock +45 -0
- data/gemfiles/mongoid_2.0.0.gemfile +9 -0
- data/gemfiles/mongoid_2.0.0.gemfile.lock +49 -0
- data/gemfiles/mongoid_2.1.4.gemfile +9 -0
- data/gemfiles/mongoid_2.1.4.gemfile.lock +47 -0
- data/gemfiles/mongoid_2.2.4.gemfile +9 -0
- data/gemfiles/mongoid_2.2.4.gemfile.lock +47 -0
- data/gemfiles/mongoid_2.3.3.gemfile +9 -0
- data/gemfiles/mongoid_2.3.3.gemfile.lock +47 -0
- data/gemfiles/mongoid_2.4.0.gemfile +9 -0
- data/gemfiles/mongoid_2.4.0.gemfile.lock +47 -0
- data/gemfiles/mongoid_2.4.10.gemfile +9 -0
- data/gemfiles/mongoid_2.4.10.gemfile.lock +47 -0
- data/gemfiles/mongoid_2.5.2.gemfile +9 -0
- data/gemfiles/mongoid_2.5.2.gemfile.lock +47 -0
- data/gemfiles/mongoid_2.6.0.gemfile +9 -0
- data/gemfiles/mongoid_2.6.0.gemfile.lock +47 -0
- data/gemfiles/mongoid_3.0.0.gemfile +8 -0
- data/gemfiles/mongoid_3.0.0.gemfile.lock +45 -0
- data/gemfiles/mongoid_3.0.22.gemfile +8 -0
- data/gemfiles/mongoid_3.0.22.gemfile.lock +45 -0
- data/gemfiles/mongoid_3.1.0.gemfile +8 -0
- data/gemfiles/mongoid_3.1.0.gemfile.lock +45 -0
- data/gemfiles/sequel_2.11.0.gemfile +9 -0
- data/gemfiles/sequel_2.11.0.gemfile.lock +33 -0
- data/gemfiles/sequel_2.12.0.gemfile +9 -0
- data/gemfiles/sequel_2.12.0.gemfile.lock +33 -0
- data/gemfiles/sequel_2.8.0.gemfile +9 -0
- data/gemfiles/sequel_2.8.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.0.0.gemfile +9 -0
- data/gemfiles/sequel_3.0.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.10.0.gemfile +9 -0
- data/gemfiles/sequel_3.10.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.13.0.gemfile +9 -0
- data/gemfiles/sequel_3.13.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.14.0.gemfile +9 -0
- data/gemfiles/sequel_3.14.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.23.0.gemfile +9 -0
- data/gemfiles/sequel_3.23.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.24.0.gemfile +9 -0
- data/gemfiles/sequel_3.24.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.29.0.gemfile +9 -0
- data/gemfiles/sequel_3.29.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.34.0.gemfile +9 -0
- data/gemfiles/sequel_3.34.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.35.0.gemfile +9 -0
- data/gemfiles/sequel_3.35.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.4.0.gemfile +9 -0
- data/gemfiles/sequel_3.4.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.44.0.gemfile +9 -0
- data/gemfiles/sequel_3.44.0.gemfile.lock +33 -0
- data/init.rb +1 -0
- data/lib/state_machine.rb +8 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/branch.rb +225 -0
- data/lib/state_machine/callback.rb +236 -0
- data/lib/state_machine/core.rb +12 -0
- data/lib/state_machine/core_ext.rb +2 -0
- data/lib/state_machine/core_ext/class/state_machine.rb +5 -0
- data/lib/state_machine/error.rb +13 -0
- data/lib/state_machine/eval_helpers.rb +87 -0
- data/lib/state_machine/event.rb +257 -0
- data/lib/state_machine/event_collection.rb +141 -0
- data/lib/state_machine/extensions.rb +149 -0
- data/lib/state_machine/graph.rb +92 -0
- data/lib/state_machine/helper_module.rb +17 -0
- data/lib/state_machine/initializers.rb +4 -0
- data/lib/state_machine/initializers/merb.rb +1 -0
- data/lib/state_machine/initializers/rails.rb +25 -0
- data/lib/state_machine/integrations.rb +121 -0
- data/lib/state_machine/integrations/active_model.rb +585 -0
- data/lib/state_machine/integrations/active_model/locale.rb +11 -0
- data/lib/state_machine/integrations/active_model/observer.rb +33 -0
- data/lib/state_machine/integrations/active_model/observer_update.rb +42 -0
- data/lib/state_machine/integrations/active_model/versions.rb +31 -0
- data/lib/state_machine/integrations/active_record.rb +552 -0
- data/lib/state_machine/integrations/active_record/locale.rb +20 -0
- data/lib/state_machine/integrations/active_record/versions.rb +123 -0
- data/lib/state_machine/integrations/base.rb +100 -0
- data/lib/state_machine/integrations/data_mapper.rb +511 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
- data/lib/state_machine/integrations/data_mapper/versions.rb +85 -0
- data/lib/state_machine/integrations/mongo_mapper.rb +389 -0
- data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
- data/lib/state_machine/integrations/mongo_mapper/versions.rb +89 -0
- data/lib/state_machine/integrations/mongoid.rb +465 -0
- data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
- data/lib/state_machine/integrations/mongoid/versions.rb +81 -0
- data/lib/state_machine/integrations/sequel.rb +486 -0
- data/lib/state_machine/integrations/sequel/versions.rb +95 -0
- data/lib/state_machine/machine.rb +2292 -0
- data/lib/state_machine/machine_collection.rb +86 -0
- data/lib/state_machine/macro_methods.rb +522 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +222 -0
- data/lib/state_machine/path.rb +120 -0
- data/lib/state_machine/path_collection.rb +90 -0
- data/lib/state_machine/state.rb +297 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/state_context.rb +138 -0
- data/lib/state_machine/transition.rb +470 -0
- data/lib/state_machine/transition_collection.rb +245 -0
- data/lib/state_machine/version.rb +3 -0
- data/lib/state_machine/yard.rb +8 -0
- data/lib/state_machine/yard/handlers.rb +12 -0
- data/lib/state_machine/yard/handlers/base.rb +32 -0
- data/lib/state_machine/yard/handlers/event.rb +25 -0
- data/lib/state_machine/yard/handlers/machine.rb +344 -0
- data/lib/state_machine/yard/handlers/state.rb +25 -0
- data/lib/state_machine/yard/handlers/transition.rb +47 -0
- data/lib/state_machine/yard/templates.rb +3 -0
- data/lib/state_machine/yard/templates/default/class/html/setup.rb +30 -0
- data/lib/state_machine/yard/templates/default/class/html/state_machines.erb +12 -0
- data/lib/tasks/state_machine.rake +1 -0
- data/lib/tasks/state_machine.rb +30 -0
- data/lib/yard-state_machine.rb +2 -0
- data/state_machine.gemspec +22 -0
- data/test/files/en.yml +17 -0
- data/test/files/switch.rb +15 -0
- data/test/functional/state_machine_test.rb +1066 -0
- data/test/test_helper.rb +7 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/branch_test.rb +969 -0
- data/test/unit/callback_test.rb +704 -0
- data/test/unit/error_test.rb +43 -0
- data/test/unit/eval_helpers_test.rb +270 -0
- data/test/unit/event_collection_test.rb +398 -0
- data/test/unit/event_test.rb +1196 -0
- data/test/unit/graph_test.rb +98 -0
- data/test/unit/helper_module_test.rb +17 -0
- data/test/unit/integrations/active_model_test.rb +1245 -0
- data/test/unit/integrations/active_record_test.rb +2551 -0
- data/test/unit/integrations/base_test.rb +104 -0
- data/test/unit/integrations/data_mapper_test.rb +2194 -0
- data/test/unit/integrations/mongo_mapper_test.rb +2026 -0
- data/test/unit/integrations/mongoid_test.rb +2309 -0
- data/test/unit/integrations/sequel_test.rb +1896 -0
- data/test/unit/integrations_test.rb +83 -0
- data/test/unit/invalid_event_test.rb +20 -0
- data/test/unit/invalid_parallel_transition_test.rb +18 -0
- data/test/unit/invalid_transition_test.rb +115 -0
- data/test/unit/machine_collection_test.rb +603 -0
- data/test/unit/machine_test.rb +3431 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +362 -0
- data/test/unit/path_collection_test.rb +266 -0
- data/test/unit/path_test.rb +485 -0
- data/test/unit/state_collection_test.rb +352 -0
- data/test/unit/state_context_test.rb +441 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +1101 -0
- data/test/unit/transition_collection_test.rb +2168 -0
- data/test/unit/transition_test.rb +1558 -0
- metadata +435 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
filename = "#{File.dirname(__FILE__)}/../active_model/locale.rb"
|
2
|
+
translations = eval(IO.read(File.expand_path(filename)), binding, filename)
|
3
|
+
translations[:en][:activerecord] = translations[:en].delete(:activemodel)
|
4
|
+
|
5
|
+
# Only ActiveRecord 2.3.5+ can pull i18n >= 0.1.3 from system-wide gems (and
|
6
|
+
# therefore possibly have I18n::VERSION available)
|
7
|
+
begin
|
8
|
+
require 'i18n/version'
|
9
|
+
rescue Exception => ex
|
10
|
+
end unless ::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 5)
|
11
|
+
|
12
|
+
# Only i18n 0.4.0+ has the new %{key} syntax
|
13
|
+
if !defined?(I18n::VERSION) || I18n::VERSION < '0.4.0'
|
14
|
+
translations[:en][:activerecord][:errors][:messages].each do |key, message|
|
15
|
+
message.gsub!('%{', '{{')
|
16
|
+
message.gsub!('}', '}}')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
translations
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module StateMachine
|
2
|
+
module Integrations #:nodoc:
|
3
|
+
module ActiveRecord
|
4
|
+
version '2.x - 3.0.x' do
|
5
|
+
def self.active?
|
6
|
+
::ActiveRecord::VERSION::MAJOR == 2 || ::ActiveRecord::VERSION::MAJOR == 3 && ::ActiveRecord::VERSION::MINOR == 0
|
7
|
+
end
|
8
|
+
|
9
|
+
def define_static_state_initializer
|
10
|
+
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
11
|
+
def attributes_from_column_definition(*)
|
12
|
+
result = super
|
13
|
+
self.class.state_machines.initialize_states(self, :static => :force, :dynamic => false, :to => result)
|
14
|
+
result
|
15
|
+
end
|
16
|
+
end_eval
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
version '2.x' do
|
21
|
+
def self.active?
|
22
|
+
::ActiveRecord::VERSION::MAJOR == 2
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_locale
|
26
|
+
super if defined?(I18n)
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_scope(name, scope)
|
30
|
+
if owner_class.respond_to?(:named_scope)
|
31
|
+
name = name.to_sym
|
32
|
+
machine_name = self.name
|
33
|
+
|
34
|
+
# Since ActiveRecord does not allow direct access to the model
|
35
|
+
# being used within the evaluation of a dynamic named scope, the
|
36
|
+
# scope must be generated manually. It's necessary to have access
|
37
|
+
# to the model so that the state names can be translated to their
|
38
|
+
# associated values and so that inheritance is respected properly.
|
39
|
+
owner_class.named_scope(name)
|
40
|
+
owner_class.scopes[name] = lambda do |model, *states|
|
41
|
+
machine_states = model.state_machine(machine_name).states
|
42
|
+
values = states.flatten.map {|state| machine_states.fetch(state).value}
|
43
|
+
|
44
|
+
::ActiveRecord::NamedScope::Scope.new(model, :conditions => scope.call(values))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Prevent the Machine class from wrapping the scope
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
def invalidate(object, attribute, message, values = [])
|
53
|
+
if defined?(I18n)
|
54
|
+
super
|
55
|
+
else
|
56
|
+
object.errors.add(self.attribute(attribute), generate_message(message, values))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def translate(klass, key, value)
|
61
|
+
if defined?(I18n)
|
62
|
+
super
|
63
|
+
else
|
64
|
+
value ? value.to_s.humanize.downcase : 'nil'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def supports_observers?
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
def supports_validations?
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
def supports_mass_assignment_security?
|
77
|
+
true
|
78
|
+
end
|
79
|
+
|
80
|
+
def i18n_scope(klass)
|
81
|
+
:activerecord
|
82
|
+
end
|
83
|
+
|
84
|
+
def load_observer_extensions
|
85
|
+
super
|
86
|
+
::ActiveRecord::Observer.class_eval do
|
87
|
+
include StateMachine::Integrations::ActiveModel::Observer
|
88
|
+
end unless ::ActiveRecord::Observer < StateMachine::Integrations::ActiveModel::Observer
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
version '2.0 - 2.2.x' do
|
93
|
+
def self.active?
|
94
|
+
::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR < 3
|
95
|
+
end
|
96
|
+
|
97
|
+
def default_error_message_options(object, attribute, message)
|
98
|
+
{:default => @messages[message]}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
version '2.0 - 2.3.1' do
|
103
|
+
def self.active?
|
104
|
+
::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 2)
|
105
|
+
end
|
106
|
+
|
107
|
+
def ancestors_for(klass)
|
108
|
+
klass.self_and_descendents_from_active_record
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
version '2.3.2 - 2.3.x' do
|
113
|
+
def self.active?
|
114
|
+
::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR == 3 && ::ActiveRecord::VERSION::TINY >= 2
|
115
|
+
end
|
116
|
+
|
117
|
+
def ancestors_for(klass)
|
118
|
+
klass.self_and_descendants_from_active_record
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module StateMachine
|
2
|
+
module Integrations
|
3
|
+
# Provides a set of base helpers for managing individual integrations
|
4
|
+
module Base
|
5
|
+
module ClassMethods
|
6
|
+
# The default options to use for state machines using this integration
|
7
|
+
attr_reader :defaults
|
8
|
+
|
9
|
+
# The name of the integration
|
10
|
+
def integration_name
|
11
|
+
@integration_name ||= begin
|
12
|
+
name = self.name.split('::').last
|
13
|
+
name.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
14
|
+
name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
15
|
+
name.downcase!
|
16
|
+
name.to_sym
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Whether this integration is available for the current library. This
|
21
|
+
# is only true if the ORM that the integration is for is currently
|
22
|
+
# defined.
|
23
|
+
def available?
|
24
|
+
matching_ancestors.any? && Object.const_defined?(matching_ancestors[0].split('::')[0])
|
25
|
+
end
|
26
|
+
|
27
|
+
# The list of ancestor names that cause this integration to matched.
|
28
|
+
def matching_ancestors
|
29
|
+
[]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Whether the integration should be used for the given class.
|
33
|
+
def matches?(klass)
|
34
|
+
matches_ancestors?(klass.ancestors.map {|ancestor| ancestor.name})
|
35
|
+
end
|
36
|
+
|
37
|
+
# Whether the integration should be used for the given list of ancestors.
|
38
|
+
def matches_ancestors?(ancestors)
|
39
|
+
(ancestors & matching_ancestors).any?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Tracks the various version overrides for an integration
|
43
|
+
def versions
|
44
|
+
@versions ||= []
|
45
|
+
end
|
46
|
+
|
47
|
+
# Creates a new version override for an integration. When this
|
48
|
+
# integration is activated, each version that is marked as active will
|
49
|
+
# also extend the integration.
|
50
|
+
#
|
51
|
+
# == Example
|
52
|
+
#
|
53
|
+
# module StateMachine
|
54
|
+
# module Integrations
|
55
|
+
# module ORMLibrary
|
56
|
+
# version '0.2.x - 0.3.x' do
|
57
|
+
# def self.active?
|
58
|
+
# ::ORMLibrary::VERSION >= '0.2.0' && ::ORMLibrary::VERSION < '0.4.0'
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# def invalidate(object, attribute, message, values = [])
|
62
|
+
# # Override here...
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
# end
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# In the above example, a version override is defined for the ORMLibrary
|
70
|
+
# integration when the version is between 0.2.x and 0.3.x.
|
71
|
+
def version(name, &block)
|
72
|
+
versions << mod = Module.new(&block)
|
73
|
+
mod
|
74
|
+
end
|
75
|
+
|
76
|
+
# The path to the locale file containing translations for this
|
77
|
+
# integration. This file will only exist for integrations that actually
|
78
|
+
# support i18n.
|
79
|
+
def locale_path
|
80
|
+
path = "#{File.dirname(__FILE__)}/#{integration_name}/locale.rb"
|
81
|
+
path if File.exist?(path)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Extends the given object with any version overrides that are currently
|
85
|
+
# active
|
86
|
+
def extended(base)
|
87
|
+
versions.each do |version|
|
88
|
+
base.extend(version) if version.active?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
extend ClassMethods
|
94
|
+
|
95
|
+
def self.included(base) #:nodoc:
|
96
|
+
base.class_eval { extend ClassMethods }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,511 @@
|
|
1
|
+
module StateMachine
|
2
|
+
module Integrations #:nodoc:
|
3
|
+
# Adds support for integrating state machines with DataMapper resources.
|
4
|
+
#
|
5
|
+
# == Examples
|
6
|
+
#
|
7
|
+
# Below is an example of a simple state machine defined within a
|
8
|
+
# DataMapper resource:
|
9
|
+
#
|
10
|
+
# class Vehicle
|
11
|
+
# include DataMapper::Resource
|
12
|
+
#
|
13
|
+
# property :id, Serial
|
14
|
+
# property :name, String
|
15
|
+
# property :state, String
|
16
|
+
#
|
17
|
+
# state_machine :initial => :parked do
|
18
|
+
# event :ignite do
|
19
|
+
# transition :parked => :idling
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# The examples in the sections below will use the above class as a
|
25
|
+
# reference.
|
26
|
+
#
|
27
|
+
# == Actions
|
28
|
+
#
|
29
|
+
# By default, the action that will be invoked when a state is transitioned
|
30
|
+
# is the +save+ action. This will cause the resource to save the changes
|
31
|
+
# made to the state machine's attribute. *Note* that if any other changes
|
32
|
+
# were made to the resource prior to transition, then those changes will
|
33
|
+
# be saved as well.
|
34
|
+
#
|
35
|
+
# For example,
|
36
|
+
#
|
37
|
+
# vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
|
38
|
+
# vehicle.name = 'Ford Explorer'
|
39
|
+
# vehicle.ignite # => true
|
40
|
+
# vehicle.reload # => #<Vehicle id=1 name="Ford Explorer" state="idling">
|
41
|
+
#
|
42
|
+
# == Events
|
43
|
+
#
|
44
|
+
# As described in StateMachine::InstanceMethods#state_machine, event
|
45
|
+
# attributes are created for every machine that allow transitions to be
|
46
|
+
# performed automatically when the object's action (in this case, :save)
|
47
|
+
# is called.
|
48
|
+
#
|
49
|
+
# In DataMapper, these automated events are run in the following order:
|
50
|
+
# * before validation - If validation feature loaded, run before callbacks and persist new states, then validate
|
51
|
+
# * before save - If validation feature was skipped/not loaded, run before callbacks and persist new states, then save
|
52
|
+
# * after save - Run after callbacks
|
53
|
+
#
|
54
|
+
# For example,
|
55
|
+
#
|
56
|
+
# vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
|
57
|
+
# vehicle.state_event # => nil
|
58
|
+
# vehicle.state_event = 'invalid'
|
59
|
+
# vehicle.valid? # => false
|
60
|
+
# vehicle.errors # => #<DataMapper::Validate::ValidationErrors:0xb7a48b54 @errors={"state_event"=>["is invalid"]}>
|
61
|
+
#
|
62
|
+
# vehicle.state_event = 'ignite'
|
63
|
+
# vehicle.valid? # => true
|
64
|
+
# vehicle.save # => true
|
65
|
+
# vehicle.state # => "idling"
|
66
|
+
# vehicle.state_event # => nil
|
67
|
+
#
|
68
|
+
# Note that this can also be done on a mass-assignment basis:
|
69
|
+
#
|
70
|
+
# vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id=1 name=nil state="idling">
|
71
|
+
# vehicle.state # => "idling"
|
72
|
+
#
|
73
|
+
# This technique is always used for transitioning states when the +save+
|
74
|
+
# action (which is the default) is configured for the machine.
|
75
|
+
#
|
76
|
+
# === Security implications
|
77
|
+
#
|
78
|
+
# Beware that public event attributes mean that events can be fired
|
79
|
+
# whenever mass-assignment is being used. If you want to prevent malicious
|
80
|
+
# users from tampering with events through URLs / forms, the attribute
|
81
|
+
# should be protected like so:
|
82
|
+
#
|
83
|
+
# class Vehicle
|
84
|
+
# include DataMapper::Resource
|
85
|
+
# ...
|
86
|
+
#
|
87
|
+
# state_machine do
|
88
|
+
# ...
|
89
|
+
# end
|
90
|
+
# protected :state_event
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# If you want to only have *some* events be able to fire via mass-assignment,
|
94
|
+
# you can build two state machines (one public and one protected) like so:
|
95
|
+
#
|
96
|
+
# class Vehicle
|
97
|
+
# include DataMapper::Resource
|
98
|
+
# ...
|
99
|
+
#
|
100
|
+
# state_machine do
|
101
|
+
# # Define private events here
|
102
|
+
# end
|
103
|
+
# protected :state_event= # Prevent access to events in the first machine
|
104
|
+
#
|
105
|
+
# # Allow both machines to share the same state
|
106
|
+
# state_machine :public_state, :attribute => :state do
|
107
|
+
# # Define public events here
|
108
|
+
# end
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# === Within DataMapper Hooks
|
112
|
+
#
|
113
|
+
# DataMapper protects against the potential for system stack errors resulting
|
114
|
+
# from infinite loops by preventing records from being saved multiple times
|
115
|
+
# within save hooks. You need to be acutely aware of this when interacting
|
116
|
+
# with state_machine within save hooks. There are two things to keep in mind:
|
117
|
+
#
|
118
|
+
# 1. You cannot run a state_machine event during an `after :save/:create` hook.
|
119
|
+
# 2. If you need to run a state_machine event during a `before :save/:create/etc.`
|
120
|
+
# hook, then you have to force the machine's action to be skipped by passing
|
121
|
+
# `false` in as an argument to the event.
|
122
|
+
#
|
123
|
+
# For example:
|
124
|
+
#
|
125
|
+
# class Vehicle
|
126
|
+
# include DataMapper::Resource
|
127
|
+
# ...
|
128
|
+
#
|
129
|
+
# state_machine :initial => :parked do
|
130
|
+
# event :ignite do
|
131
|
+
# transition :parked => :idling
|
132
|
+
# end
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# # This will allow the event to transition without attempting to save a second time
|
136
|
+
# before :create { ignite(false) }
|
137
|
+
#
|
138
|
+
# # This will never work because DataMapper will refuse to save the
|
139
|
+
# # changes since we're still inside of a transaction
|
140
|
+
# # after :create { ignite }
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# While the above will work, in reality you should typically just set the
|
144
|
+
# `state_event` attribute in `#initialize` to automatically transition an
|
145
|
+
# object on creation.
|
146
|
+
#
|
147
|
+
# == Transactions
|
148
|
+
#
|
149
|
+
# By default, the use of transactions during an event transition is
|
150
|
+
# turned off to be consistent with DataMapper. This means that if
|
151
|
+
# changes are made to the database during a before callback, but the
|
152
|
+
# transition fails to complete, those changes will *not* be rolled back.
|
153
|
+
#
|
154
|
+
# For example,
|
155
|
+
#
|
156
|
+
# class Message
|
157
|
+
# include DataMapper::Resource
|
158
|
+
#
|
159
|
+
# property :id, Serial
|
160
|
+
# property :content, String
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# Vehicle.state_machine do
|
164
|
+
# before_transition do |transition|
|
165
|
+
# Message.create(:content => transition.inspect)
|
166
|
+
# throw :halt
|
167
|
+
# end
|
168
|
+
# end
|
169
|
+
#
|
170
|
+
# vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
|
171
|
+
# vehicle.ignite # => false
|
172
|
+
# Message.all.count # => 1
|
173
|
+
#
|
174
|
+
# To turn on transactions:
|
175
|
+
#
|
176
|
+
# class Vehicle
|
177
|
+
# include DataMapper::Resource
|
178
|
+
# ...
|
179
|
+
#
|
180
|
+
# state_machine :initial => :parked, :use_transactions => true do
|
181
|
+
# ...
|
182
|
+
# end
|
183
|
+
# end
|
184
|
+
#
|
185
|
+
# == Validation errors
|
186
|
+
#
|
187
|
+
# If an event fails to successfully fire because there are no matching
|
188
|
+
# transitions for the current record, a validation error is added to the
|
189
|
+
# record's state attribute to help in determining why it failed and for
|
190
|
+
# reporting via the UI.
|
191
|
+
#
|
192
|
+
# For example,
|
193
|
+
#
|
194
|
+
# vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id=1 name=nil state="idling">
|
195
|
+
# vehicle.ignite # => false
|
196
|
+
# vehicle.errors.full_messages # => ["cannot transition via \"ignite\""]
|
197
|
+
#
|
198
|
+
# If an event fails to fire because of a validation error on the record and
|
199
|
+
# *not* because a matching transition was not available, no error messages
|
200
|
+
# will be added to the state attribute.
|
201
|
+
#
|
202
|
+
# In addition, if you're using the <tt>ignite!</tt> version of the event,
|
203
|
+
# then the failure reason (such as the current validation errors) will be
|
204
|
+
# included in the exception that gets raised when the event fails. For
|
205
|
+
# example, assuming there's a validation on a field called +name+ on the class:
|
206
|
+
#
|
207
|
+
# vehicle = Vehicle.new
|
208
|
+
# vehicle.ignite! # => StateMachine::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
|
209
|
+
#
|
210
|
+
# == Scopes
|
211
|
+
#
|
212
|
+
# To assist in filtering models with specific states, a series of class
|
213
|
+
# methods are defined on the model for finding records with or without a
|
214
|
+
# particular set of states.
|
215
|
+
#
|
216
|
+
# These named scopes are the functional equivalent of the following
|
217
|
+
# definitions:
|
218
|
+
#
|
219
|
+
# class Vehicle
|
220
|
+
# include DataMapper::Resource
|
221
|
+
#
|
222
|
+
# property :id, Serial
|
223
|
+
# property :state, String
|
224
|
+
#
|
225
|
+
# class << self
|
226
|
+
# def with_states(*states)
|
227
|
+
# all(:state => states.flatten)
|
228
|
+
# end
|
229
|
+
# alias_method :with_state, :with_states
|
230
|
+
#
|
231
|
+
# def without_states(*states)
|
232
|
+
# all(:state.not => states.flatten)
|
233
|
+
# end
|
234
|
+
# alias_method :without_state, :without_states
|
235
|
+
# end
|
236
|
+
# end
|
237
|
+
#
|
238
|
+
# *Note*, however, that the states are converted to their stored values
|
239
|
+
# before being passed into the query.
|
240
|
+
#
|
241
|
+
# Because of the way scopes work in DataMapper, they can be chained like
|
242
|
+
# so:
|
243
|
+
#
|
244
|
+
# Vehicle.with_state(:parked).all(:order => [:id.desc])
|
245
|
+
#
|
246
|
+
# Note that states can also be referenced by the string version of their
|
247
|
+
# name:
|
248
|
+
#
|
249
|
+
# Vehicle.with_state('parked')
|
250
|
+
#
|
251
|
+
# == Callbacks / Observers
|
252
|
+
#
|
253
|
+
# All before/after transition callbacks defined for DataMapper resources
|
254
|
+
# behave in the same way that other DataMapper hooks behave. Rather than
|
255
|
+
# passing in the record as an argument to the callback, the callback is
|
256
|
+
# instead bound to the object and evaluated within its context.
|
257
|
+
#
|
258
|
+
# For example,
|
259
|
+
#
|
260
|
+
# class Vehicle
|
261
|
+
# include DataMapper::Resource
|
262
|
+
#
|
263
|
+
# property :id, Serial
|
264
|
+
# property :state, String
|
265
|
+
#
|
266
|
+
# state_machine :initial => :parked do
|
267
|
+
# before_transition any => :idling do
|
268
|
+
# put_on_seatbelt
|
269
|
+
# end
|
270
|
+
#
|
271
|
+
# before_transition do |transition|
|
272
|
+
# # log message
|
273
|
+
# end
|
274
|
+
#
|
275
|
+
# event :ignite do
|
276
|
+
# transition :parked => :idling
|
277
|
+
# end
|
278
|
+
# end
|
279
|
+
#
|
280
|
+
# def put_on_seatbelt
|
281
|
+
# ...
|
282
|
+
# end
|
283
|
+
# end
|
284
|
+
#
|
285
|
+
# Note, also, that the transition can be accessed by simply defining
|
286
|
+
# additional arguments in the callback block.
|
287
|
+
#
|
288
|
+
# In addition to support for DataMapper-like hooks, there is additional
|
289
|
+
# support for DataMapper observers. See StateMachine::Integrations::DataMapper::Observer
|
290
|
+
# for more information.
|
291
|
+
#
|
292
|
+
# === Failure callbacks
|
293
|
+
#
|
294
|
+
# +after_failure+ callbacks allow you to execute behaviors when a transition
|
295
|
+
# is allowed, but fails to save. This could be useful for something like
|
296
|
+
# auditing transition attempts. Since callbacks run within transactions in
|
297
|
+
# DataMapper, a save failure will cause any records that get created in
|
298
|
+
# your callback to roll back. *Note* that this is only a problem if the
|
299
|
+
# machine is configured to use transactions. If it is, you can work around
|
300
|
+
# this issue like so:
|
301
|
+
#
|
302
|
+
# DataMapper.setup(:default, 'mysql://localhost/app')
|
303
|
+
# DataMapper.setup(:logs, 'mysql://localhost/app')
|
304
|
+
#
|
305
|
+
# class TransitionLog
|
306
|
+
# include DataMapper::Resource
|
307
|
+
# end
|
308
|
+
#
|
309
|
+
# class Vehicle < ActiveRecord::Base
|
310
|
+
# include DataMapper::Resource
|
311
|
+
#
|
312
|
+
# state_machine :use_transactions => true do
|
313
|
+
# after_failure do |transition|
|
314
|
+
# DataMapper.repository(:logs) do
|
315
|
+
# TransitionLog.create(:vehicle => vehicle, :transition => transition)
|
316
|
+
# end
|
317
|
+
# end
|
318
|
+
#
|
319
|
+
# ...
|
320
|
+
# end
|
321
|
+
# end
|
322
|
+
#
|
323
|
+
# The failure callback creates +TransitionLog+ records using a second
|
324
|
+
# connection to the database, allowing them to be saved without being
|
325
|
+
# affected by rollbacks in the +Vehicle+ resource's transaction.
|
326
|
+
#
|
327
|
+
# === Callback Order
|
328
|
+
#
|
329
|
+
# Callbacks occur in the following order. Callbacks specific to state_machine
|
330
|
+
# are bolded. The remaining callbacks are part of ActiveRecord.
|
331
|
+
#
|
332
|
+
# * (-) save
|
333
|
+
# * (-) begin transaction (if enabled)
|
334
|
+
# * (1) *before_transition*
|
335
|
+
# * (2) before :valid?
|
336
|
+
# * (-) valid?
|
337
|
+
# * (3) after :valid?
|
338
|
+
# * (4) before :save
|
339
|
+
# * (-) save
|
340
|
+
# * (5) before :create
|
341
|
+
# * (-) create
|
342
|
+
# * (6) after :create
|
343
|
+
# * (7) after :save
|
344
|
+
# * (8) *after_transition*
|
345
|
+
# * (-) end transaction (if enabled)
|
346
|
+
module DataMapper
|
347
|
+
include Base
|
348
|
+
|
349
|
+
require 'state_machine/integrations/data_mapper/versions'
|
350
|
+
|
351
|
+
# The default options to use for state machines using this integration
|
352
|
+
@defaults = {:action => :save, :use_transactions => false}
|
353
|
+
|
354
|
+
# Classes that include DataMapper::Resource will automatically use the
|
355
|
+
# DataMapper integration.
|
356
|
+
def self.matching_ancestors
|
357
|
+
%w(DataMapper::Resource)
|
358
|
+
end
|
359
|
+
|
360
|
+
# Loads additional files specific to DataMapper
|
361
|
+
def self.extended(base) #:nodoc:
|
362
|
+
require 'dm-core/version' unless ::DataMapper.const_defined?('VERSION')
|
363
|
+
super
|
364
|
+
end
|
365
|
+
|
366
|
+
# Adds a validation error to the given object
|
367
|
+
def invalidate(object, attribute, message, values = [])
|
368
|
+
object.errors.add(self.attribute(attribute), generate_message(message, values)) if supports_validations?
|
369
|
+
end
|
370
|
+
|
371
|
+
# Describes the current validation errors on the given object. If none
|
372
|
+
# are specific, then the default error is interpeted as a "halt".
|
373
|
+
def errors_for(object)
|
374
|
+
if object.errors.empty?
|
375
|
+
'Transition halted'
|
376
|
+
else
|
377
|
+
errors = []
|
378
|
+
object.errors.each_pair do |field_name, field_errors|
|
379
|
+
field_errors.each {|error| errors << "#{field_name} #{error}"}
|
380
|
+
end
|
381
|
+
errors * ', '
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
# Resets any errors previously added when invalidating the given object
|
386
|
+
def reset(object)
|
387
|
+
object.errors.clear if supports_validations?
|
388
|
+
end
|
389
|
+
|
390
|
+
protected
|
391
|
+
# Initializes class-level extensions and defaults for this machine
|
392
|
+
def after_initialize
|
393
|
+
super
|
394
|
+
load_observer_extensions
|
395
|
+
end
|
396
|
+
|
397
|
+
# Loads extensions to DataMapper's Observers
|
398
|
+
def load_observer_extensions
|
399
|
+
require 'state_machine/integrations/data_mapper/observer' if ::DataMapper.const_defined?('Observer')
|
400
|
+
end
|
401
|
+
|
402
|
+
# Is validation support currently loaded?
|
403
|
+
def supports_validations?
|
404
|
+
@supports_validations ||= ::DataMapper.const_defined?('Validate')
|
405
|
+
end
|
406
|
+
|
407
|
+
# Gets the db default for the machine's attribute
|
408
|
+
def owner_class_attribute_default
|
409
|
+
attribute_property && attribute_property.default
|
410
|
+
end
|
411
|
+
|
412
|
+
# Gets the property for this machine's attribute (if it exists)
|
413
|
+
def attribute_property
|
414
|
+
owner_class.properties.detect {|property| property.name == attribute}
|
415
|
+
end
|
416
|
+
|
417
|
+
# Pluralizes the name using the built-in inflector
|
418
|
+
def pluralize(word)
|
419
|
+
::DataMapper::Inflector.pluralize(word.to_s)
|
420
|
+
end
|
421
|
+
|
422
|
+
# Defines an initialization hook into the owner class for setting the
|
423
|
+
# initial state of the machine *before* any attributes are set on the
|
424
|
+
# object
|
425
|
+
def define_state_initializer
|
426
|
+
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
427
|
+
def initialize(*args)
|
428
|
+
self.class.state_machines.initialize_states(self, :static => :force) { super }
|
429
|
+
end
|
430
|
+
end_eval
|
431
|
+
end
|
432
|
+
|
433
|
+
# Skips defining reader/writer methods since this is done automatically
|
434
|
+
def define_state_accessor
|
435
|
+
owner_class.property(attribute, String) unless attribute_property
|
436
|
+
|
437
|
+
if supports_validations?
|
438
|
+
name = self.name
|
439
|
+
owner_class.validates_with_block(attribute) do
|
440
|
+
machine = self.class.state_machine(name)
|
441
|
+
machine.states.match(self) ? true : [false, machine.generate_message(:invalid)]
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
# Adds hooks into validation for automatically firing events
|
447
|
+
def define_action_helpers
|
448
|
+
if action_hook == :save
|
449
|
+
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
450
|
+
def save(*)
|
451
|
+
result = self.class.state_machines.transitions(self, :save).perform { super }
|
452
|
+
assert_save_successful(:save, result)
|
453
|
+
result
|
454
|
+
end
|
455
|
+
|
456
|
+
def save!(*)
|
457
|
+
result = self.class.state_machines.transitions(self, :save).perform { super }
|
458
|
+
assert_save_successful(:save!, result)
|
459
|
+
result
|
460
|
+
end
|
461
|
+
|
462
|
+
def save_self(*)
|
463
|
+
self.class.state_machines.transitions(self, :save).perform { super }
|
464
|
+
end
|
465
|
+
end_eval
|
466
|
+
|
467
|
+
define_validation_hook
|
468
|
+
else
|
469
|
+
super
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
# Adds hooks into validation for automatically firing events
|
474
|
+
def define_validation_hook
|
475
|
+
if supports_validations?
|
476
|
+
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
477
|
+
def valid?(*)
|
478
|
+
self.class.state_machines.transitions(self, :save, :after => false).perform { super }
|
479
|
+
end
|
480
|
+
end_eval
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
# Creates a scope for finding records *with* a particular state or
|
485
|
+
# states for the attribute
|
486
|
+
def create_with_scope(name)
|
487
|
+
lambda {|resource, values| resource.all(attribute => values)}
|
488
|
+
end
|
489
|
+
|
490
|
+
# Creates a scope for finding records *without* a particular state or
|
491
|
+
# states for the attribute
|
492
|
+
def create_without_scope(name)
|
493
|
+
lambda {|resource, values| resource.all(attribute.to_sym.not => values)}
|
494
|
+
end
|
495
|
+
|
496
|
+
# Runs a new database transaction, rolling back any changes if the
|
497
|
+
# yielded block fails (i.e. returns false).
|
498
|
+
def transaction(object)
|
499
|
+
object.class.transaction {|t| t.rollback unless yield}
|
500
|
+
end
|
501
|
+
|
502
|
+
# Creates a new callback in the callback chain, always ensuring that
|
503
|
+
# it's configured to bind to the object as this is the convention for
|
504
|
+
# DataMapper/Extlib callbacks
|
505
|
+
def add_callback(type, options, &block)
|
506
|
+
options[:bind_to_object] = true
|
507
|
+
super
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|