rule 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rule.gemspec
4
+ gemspec
@@ -0,0 +1,64 @@
1
+ = Rule
2
+
3
+ == What is it?
4
+
5
+ Rule is a state machine / rule engine. It does things.
6
+
7
+ == How do I get started?
8
+
9
+ 1. Add it to your gemfile.
10
+
11
+ gem "rule", :git => "https://github.com/innovatis/rule"
12
+
13
+ 2. Add a "state" column and this code to your model:
14
+
15
+ rule_engine MyAwesomeRuleEngine
16
+
17
+ 3. create a file at app/rule_engines/my_awesome_rule_engine.rb
18
+
19
+ class MyAwesomeRuleEngine < Rule::Engine::Base
20
+
21
+ state :new
22
+ state :ongoing
23
+ state :closed
24
+
25
+ initial_state :new
26
+
27
+ terminal_state :closed
28
+
29
+ transition :new, :ongoing
30
+ validate IsInProgressRule
31
+ end
32
+
33
+ transition :ongoing, :closed
34
+ assert_presence_of object.closed_at, "Closed At"
35
+ end
36
+
37
+ end
38
+
39
+ 4. Create a file at app/rules/is_in_progress_rule.rb
40
+
41
+ class IsInProgressRule < Rule::Base
42
+ def validate
43
+ @object.in_progress.present?
44
+ end
45
+ end
46
+
47
+ 5. Set your application up to run the rules.
48
+
49
+ # wherever it makes sense...
50
+ my_object.run_rules # advances state as far as possible and automatically saves
51
+
52
+ == Explain all that stuff.
53
+
54
+ Okay.
55
+
56
+ The Rule::Engine::Base subclass defines states and transitions. Each transition block contains a number of assertions and an optional priority. If any of the assertions does not pass, the state will not follow this transition. The priority decides which transition to use in the case of two possible transitions out of the current state both being valid. Use it by specifying "priority :high" or "priority :low" in the transition block.
57
+
58
+ It often makes sense to bundle a group of assertions into a Rule class. These are defined in app/rules, and have a `validate` method containing assertions. In the future, rules will also have callbacks that fire when the state is advanced through a transition including that rule.
59
+
60
+ If this didn't answer your questions, throw things at Burke until he explains.
61
+
62
+ == Copyright
63
+
64
+ Copyright (c) 2011 Burke Libbey / Innovatis. MIT License.
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,18 @@
1
+ module Rule
2
+ end
3
+
4
+ path = File.dirname(__FILE__)
5
+ $:.unshift(path) unless $:.include?(path)
6
+
7
+ require 'rule/assertions'
8
+ require 'rule/base'
9
+ require 'rule/allow'
10
+ require 'rule/disallow'
11
+ require 'rule/engine/base'
12
+ require 'rule/engine/invalid_transition'
13
+ require 'rule/engine/state'
14
+ require 'rule/engine/transition'
15
+
16
+ require 'rule/active_record_extensions'
17
+ require 'rule/engine/active_record'
18
+ require 'rule/railtie'
@@ -0,0 +1,12 @@
1
+ class ActiveRecord::Base
2
+ def self.rule_engine(engine, opts={})
3
+ include(Rule::Engine::ActiveRecord)
4
+ column = opts[:column] || :state
5
+ if opts.delete(:before_save)
6
+ self.before_save :run_rules
7
+ end
8
+
9
+ @__rule_engine = engine.new(column)
10
+ end
11
+ end
12
+
@@ -0,0 +1,10 @@
1
+ module Rule
2
+ class Allow < Base
3
+ def validate
4
+ return true
5
+ end
6
+ end
7
+ end
8
+
9
+
10
+
@@ -0,0 +1,21 @@
1
+ module Rule
2
+ module Assertions
3
+ def assert(assertion, message)
4
+ unless assertion
5
+ add_error message
6
+ end
7
+ end
8
+
9
+ def assert_presence_of(thing, name)
10
+ unless thing.present?
11
+ add_error "#{name} must be provided"
12
+ end
13
+ end
14
+
15
+ def assert_absence_of(thing, name)
16
+ unless thing.blank?
17
+ add_error "#{name} must be blank"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Rule
2
+
3
+ class Base
4
+ attr_reader :object
5
+ def initialize(object)
6
+ @object = object
7
+ @errors = []
8
+ end
9
+
10
+ def add_error(error)
11
+ @errors << error
12
+ end
13
+
14
+ def pass?
15
+ validate != false and @errors.none?
16
+ end
17
+
18
+ include Rule::Assertions
19
+ end
20
+ end
21
+
@@ -0,0 +1,10 @@
1
+ module Rule
2
+ class Disallow < Base
3
+ def validate
4
+ return false
5
+ end
6
+ end
7
+ end
8
+
9
+
10
+
@@ -0,0 +1,27 @@
1
+ require 'active_support/concern'
2
+
3
+ module Rule
4
+ module Engine
5
+ module ActiveRecord
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+
10
+ end
11
+
12
+ module InstanceMethods
13
+ def run_rules
14
+ engine = self.class.instance_variable_get("@__rule_engine")
15
+ engine.run!(self)
16
+ end
17
+ end
18
+
19
+ module ClassMethods
20
+
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+
27
+
@@ -0,0 +1,63 @@
1
+ module Rule
2
+ module Engine
3
+ class Base
4
+
5
+ def initialize(column)
6
+ @column = column
7
+ end
8
+
9
+ def self.state(state)
10
+ find_or_create_state(state)
11
+ end
12
+
13
+ def self.initial_state(state)
14
+ @initial_state = find_or_create_state(state)
15
+ end
16
+
17
+ def self.terminal_state(state)
18
+ @terminal_states ||= []
19
+ @terminal_states << find_or_create_state(state)
20
+ end
21
+
22
+ def self.transition(from, to, &blk)
23
+ from_state = find_state!(from)
24
+ to_state = find_state!(to)
25
+
26
+ from_state.add_transition(to_state, blk)
27
+ end
28
+
29
+ def run!(object)
30
+ # state = self.class.find_state(object.send(@column).try(:to_sym)) || self.class.instance_variable_get("@initial_state")
31
+ # Revalidate from initial state each time. This is more along the lines of the behaviour dave wants.
32
+ state = self.class.instance_variable_get("@initial_state")
33
+ loop do
34
+ prev_state = state
35
+ state = prev_state.next_state(object)
36
+ break if prev_state == state
37
+ end
38
+
39
+ object.send("#{@column}=", state.name)
40
+ end
41
+
42
+ private #################################################################
43
+
44
+ def self.find_or_create_state(name)
45
+ @states ||= []
46
+ unless state = @states.find { |state| state.name == name }
47
+ state = Rule::Engine::State.new(name)
48
+ @states << state
49
+ end
50
+ state
51
+ end
52
+
53
+ def self.find_state(name)
54
+ @states.find { |state| state.name == name }
55
+ end
56
+
57
+ def self.find_state!(name)
58
+ find_state(name) or raise "State #{name} not declared"
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ class Rule::Engine::InvalidTransition < StandardError
2
+
3
+ end
@@ -0,0 +1,46 @@
1
+ module Rule
2
+ module Engine
3
+ class State
4
+
5
+ attr_reader :name
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ @transitions = []
10
+ end
11
+
12
+ def add_transition(to, blk)
13
+ @transitions << Transition.new(self, to, blk)
14
+ end
15
+
16
+ def valid_transitions(object)
17
+ @transitions.map { |transition|
18
+ begin
19
+ transition.run!(object)
20
+ rescue Rule::Engine::InvalidTransition
21
+ nil
22
+ else
23
+ transition
24
+ end
25
+ }.compact
26
+ end
27
+
28
+ def next_state(object)
29
+ transitions = valid_transitions(object)
30
+ if transitions.any?
31
+ return transitions.
32
+ sort_by{|transition| transition.instance_variable_get("@priority")}.
33
+ last.
34
+ to_state
35
+ else
36
+ return self
37
+ end
38
+ end
39
+
40
+
41
+ end
42
+ end
43
+ end
44
+
45
+
46
+
@@ -0,0 +1,59 @@
1
+ module Rule
2
+ module Engine
3
+ class Transition
4
+ attr_reader :from, :to, :action, :object
5
+ alias_method :to_state, :to
6
+ alias_method :from_state, :from
7
+
8
+ PRIORITIES = {
9
+ :min => 0,
10
+ :very_low => 1,
11
+ :low => 3,
12
+ :normal => 5,
13
+ :high => 7,
14
+ :very_high => 9,
15
+ :max => 10
16
+ }
17
+
18
+ def initialize(from, to, action)
19
+ @from = from
20
+ @to = to
21
+ @action = action
22
+ @priority = PRIORITIES[:normal]
23
+ end
24
+
25
+ def run!(object)
26
+ @object = object
27
+ action.bind(self).call
28
+ end
29
+
30
+ def add_error(error)
31
+ raise Rule::Engine::InvalidTransition
32
+ end
33
+
34
+ include Rule::Assertions
35
+
36
+ def validate(rule_klass)
37
+ unless rule_klass.new(@object).pass?
38
+ raise Rule::Engine::InvalidTransition
39
+ end
40
+ end
41
+
42
+ def priority(priority)
43
+ case priority
44
+ when Numeric
45
+ @priority = priority
46
+ when Symbol
47
+ @priority = PRIORITIES[:normal]
48
+ else
49
+ raise "Invalid priority"
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+
57
+
58
+
59
+
@@ -0,0 +1,9 @@
1
+ module Rule
2
+ class Railtie < Rails::Railtie
3
+ initializer 'rule.initialize' do |app|
4
+ app.config.autoload_paths += ["#{Rails.root}/app/rules/", "#{Rails.root}/app/rule_engines/"]
5
+ end
6
+ end
7
+ end
8
+
9
+
@@ -0,0 +1,3 @@
1
+ module Rule
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rule/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rule"
7
+ s.version = Rule::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Burke Libbey"]
10
+ s.email = ["burke@burkelibbey.org"]
11
+ s.homepage = ""
12
+ s.summary = %q{Rules on Rails}
13
+ s.description = %q{Rule Engine sort of thing for Rails.}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rule
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Burke Libbey
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-21 00:00:00.000000000Z
13
+ dependencies: []
14
+ description: Rule Engine sort of thing for Rails.
15
+ email:
16
+ - burke@burkelibbey.org
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - README.rdoc
24
+ - Rakefile
25
+ - lib/rule.rb
26
+ - lib/rule/active_record_extensions.rb
27
+ - lib/rule/allow.rb
28
+ - lib/rule/assertions.rb
29
+ - lib/rule/base.rb
30
+ - lib/rule/disallow.rb
31
+ - lib/rule/engine/active_record.rb
32
+ - lib/rule/engine/base.rb
33
+ - lib/rule/engine/invalid_transition.rb
34
+ - lib/rule/engine/state.rb
35
+ - lib/rule/engine/transition.rb
36
+ - lib/rule/railtie.rb
37
+ - lib/rule/version.rb
38
+ - rule.gemspec
39
+ homepage: ''
40
+ licenses: []
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubyforge_project:
59
+ rubygems_version: 1.8.6
60
+ signing_key:
61
+ specification_version: 3
62
+ summary: Rules on Rails
63
+ test_files: []