aquam 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 756ccfa62e8a668e3f79a7a14d86a3d754a1f9fe
4
+ data.tar.gz: ddcad78f959438aecdba5ceab40e851b8dbcd95e
5
+ SHA512:
6
+ metadata.gz: 3c30391f82ffa5d1291a9d54ac0d76ca3dadbbbadec02e6a9ac1e369b406534ae968b2901c8650fa401d6391eec81089d7abb249d5e529d520959038dac824f6
7
+ data.tar.gz: 134d43c549081bd25537ee095fabb793843d4d859ac33ca17014a4c64e37888a30dfee6d02dca0cf03b42d0c56cdb2dc676b7fbda2e195b1eeeea93740d93be8
@@ -0,0 +1 @@
1
+ # aquam
@@ -0,0 +1,19 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'aquam'
3
+ s.version = '0.0.1'
4
+ s.date = Time.now.strftime('%Y-%m-%d')
5
+ s.summary = 'DSL to define State Machines'
6
+ s.description = 'Aquam adds a DSL and validations to define a State Machine'
7
+ s.authors = ['Emiliano Mancuso']
8
+ s.email = ['emiliano.mancuso@gmail.com']
9
+ s.homepage = 'http://github.com/emancu/aquam'
10
+ s.license = 'MIT'
11
+
12
+ s.files = Dir[
13
+ 'README.md',
14
+ 'rakefile',
15
+ 'lib/**/*.rb',
16
+ '*.gemspec'
17
+ ]
18
+ s.test_files = Dir['test/*.*']
19
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'aquam/errors'
2
+ require_relative 'aquam/machine'
3
+ require_relative 'aquam/state'
4
+
5
+ module Aquam
6
+ VERSION = '0.0.1'
7
+ end
@@ -0,0 +1,14 @@
1
+ module Aquam
2
+ class InvalidStateError < StandardError; end
3
+ class InvalidEventError < StandardError; end
4
+ class InvalidTransitionError < StandardError; end
5
+ class InvalidStateMachineError < StandardError; end
6
+
7
+ class FailedTransitionError < StandardError
8
+ attr_reader :errors
9
+
10
+ def initialize(errors = {})
11
+ @errors = errors
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module Aquam
2
+ class EventTransitions
3
+ def initialize(machine, event_name, &block)
4
+ @machine = machine
5
+ @event_name = event_name
6
+ instance_eval(&block)
7
+ end
8
+
9
+ def transition(from:, to:)
10
+ @machine.transition(from, to, @event_name)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ require_relative 'machine_class_methods'
2
+
3
+ module Aquam
4
+ class Machine
5
+ extend Aquam::MachineClassMethods
6
+
7
+ attr_accessor :object
8
+
9
+ def initialize(object)
10
+ @object = object
11
+ end
12
+
13
+ def valid_state?
14
+ self.class.valid_state? attribute
15
+ end
16
+
17
+ def valid_event?(event)
18
+ self.class.valid_event? event
19
+ end
20
+
21
+ def valid_transition?(event)
22
+ self.class.events[event].key? attribute
23
+ end
24
+
25
+ def current_state
26
+ fail Aquam::InvalidStateError unless valid_state?
27
+
28
+ self.class.states[attribute].new object
29
+ end
30
+
31
+ def trigger(event, *args)
32
+ state = current_state
33
+
34
+ fail Aquam::InvalidEventError unless valid_event? event
35
+ fail Aquam::InvalidTransitionError unless valid_transition? event
36
+
37
+ state.send event, *args
38
+
39
+ current_state
40
+ end
41
+
42
+ private
43
+
44
+ def attribute
45
+ object.send(self.class.attribute.to_sym).to_sym
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,39 @@
1
+ require_relative 'event_transitions'
2
+
3
+ module Aquam
4
+ module MachineClassMethods
5
+ def attribute(name = nil)
6
+ name ? @attribute = name : @attribute ||= :state
7
+ end
8
+
9
+ def states
10
+ @states ||= {}
11
+ end
12
+
13
+ def events
14
+ @event ||= Hash.new { |hash, key| hash[key] = {} }
15
+ end
16
+
17
+ def state(name, klass)
18
+ states[name] = klass
19
+ end
20
+
21
+ def event(name, &block)
22
+ Aquam::EventTransitions.new self, name, &block
23
+ end
24
+
25
+ def transition(from, to, event_name)
26
+ fail Aquam::InvalidStateError if !valid_state?(from) || !valid_state?(to)
27
+
28
+ events[event_name][from] = to
29
+ end
30
+
31
+ def valid_state?(state)
32
+ states.keys.include? state
33
+ end
34
+
35
+ def valid_event?(event)
36
+ events.keys.include? event
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,46 @@
1
+ module Aquam
2
+ class State
3
+ # I have to use a class variable because it **must** be the same value
4
+ # across all the children calsses.
5
+ #
6
+ # Also I need it defined always
7
+ @@state_machine = nil
8
+
9
+ def self.state_machine(state_machine = nil)
10
+ if state_machine && !@@state_machine
11
+ validate_state_machine state_machine
12
+
13
+ @@state_machine = state_machine
14
+ define_event_methods
15
+ end
16
+
17
+ @@state_machine
18
+ end
19
+
20
+ def initialize(object)
21
+ @object = object
22
+ end
23
+
24
+ def state_machine
25
+ self.class.state_machine || fail(Aquam::InvalidStateMachineError)
26
+ end
27
+
28
+ private
29
+
30
+ def self.define_event_methods
31
+ state_machine.events.keys.each do |event|
32
+ define_method event do
33
+ fail Aquam::InvalidTransitionError
34
+ end
35
+ end
36
+ end
37
+ private_class_method :define_event_methods
38
+
39
+ def self.validate_state_machine(state_machine)
40
+ unless state_machine.ancestors.include? Aquam::Machine
41
+ fail Aquam::InvalidStateMachineError
42
+ end
43
+ end
44
+ private_class_method :validate_state_machine
45
+ end
46
+ end
@@ -0,0 +1,8 @@
1
+ task :default => :test
2
+
3
+ desc 'Run tests'
4
+ task :test do
5
+ require File.expand_path("./test/helper", File.dirname(__FILE__))
6
+
7
+ Dir["test/**/*_test.rb"].each { |file| load file }
8
+ end
@@ -0,0 +1,43 @@
1
+ require 'aquam'
2
+
3
+ class DoorState < Aquam::State; end
4
+
5
+ class OpenedDoorState < DoorState
6
+ def close
7
+ @object.state = :closed
8
+ end
9
+ end
10
+
11
+ class ClosedDoorState < DoorState
12
+ def open
13
+ @object.state = :opened
14
+ end
15
+ end
16
+
17
+ class DoorStateMachine < Aquam::Machine
18
+ state :opened, OpenedDoorState
19
+ state :closed, ClosedDoorState
20
+
21
+ event :open do
22
+ transition from: :closed, to: :opened
23
+ end
24
+
25
+ event :close do
26
+ transition from: :opened, to: :closed
27
+ end
28
+
29
+ event :knock do
30
+ transition from: :opened, to: :opened
31
+ transition from: :closed, to: :closed
32
+ end
33
+ end
34
+
35
+ class Door
36
+ attr_accessor :state
37
+ attr_reader :machine
38
+
39
+ def initialize
40
+ @state = :closed
41
+ @machine = DoorStateMachine.new self
42
+ end
43
+ end
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
2
+
3
+ require 'minitest/autorun'
4
+ require 'aquam'
5
+
6
+ def deny(condition, message = 'Expected condition to be unsatisfied')
7
+ assert !condition, message
8
+ end
@@ -0,0 +1,144 @@
1
+ require_relative 'helper'
2
+ require_relative 'door_state_machine'
3
+
4
+ describe Aquam::MachineClassMethods do
5
+ describe 'attribute' do
6
+ it 'uses :state as default' do
7
+ assert_equal :state, DoorStateMachine.attribute
8
+ end
9
+
10
+ it 'defines the proper attribute to access the state name' do
11
+ class NewStateMachine < Aquam::Machine
12
+ attribute :state_name
13
+ end
14
+
15
+ assert_equal :state_name, NewStateMachine.attribute
16
+
17
+ Object.send(:remove_const, :NewStateMachine)
18
+ end
19
+ end
20
+
21
+ describe 'states' do
22
+ it 'returns a hash with all the states as keys' do
23
+ assert_equal [:opened, :closed], DoorStateMachine.states.keys
24
+ end
25
+
26
+ it 'returns a hash with all the states defined' do
27
+ assert_equal OpenedDoorState, DoorStateMachine.states[:opened]
28
+ assert_equal ClosedDoorState, DoorStateMachine.states[:closed]
29
+ end
30
+
31
+ it 'checks if it is a valid state' do
32
+ assert DoorStateMachine.valid_state? :opened
33
+ deny DoorStateMachine.valid_state? :not_a_valid_state
34
+ end
35
+ end
36
+
37
+ describe 'events' do
38
+ it 'returns a hash with the events as keys' do
39
+ assert_equal [:open, :close, :knock], DoorStateMachine.events.keys
40
+ end
41
+
42
+ it 'returns a hash with the transitions of each event' do
43
+ close = { opened: :closed }
44
+ open = { closed: :opened }
45
+ knock = { opened: :opened, closed: :closed }
46
+
47
+ assert_equal close, DoorStateMachine.events[:close]
48
+ assert_equal open, DoorStateMachine.events[:open]
49
+ assert_equal knock, DoorStateMachine.events[:knock]
50
+ end
51
+
52
+ it 'checks if it is a valid event' do
53
+ assert DoorStateMachine.valid_event? :open
54
+ deny DoorStateMachine.valid_event? :not_a_valid_event
55
+ end
56
+ end
57
+
58
+ describe 'state' do
59
+ before do
60
+ class AState < Aquam::State; end
61
+ class StateMachine < Aquam::Machine; end
62
+ end
63
+
64
+ after do
65
+ Object.send(:remove_const, :AState)
66
+ Object.send(:remove_const, :StateMachine)
67
+ end
68
+
69
+ it 'defines a new state into the Machine' do
70
+ assert_equal [], StateMachine.states.keys
71
+
72
+ StateMachine.state :a, AState
73
+
74
+ assert_equal [:a], StateMachine.states.keys
75
+ assert_equal AState, StateMachine.states[:a]
76
+ end
77
+ end
78
+
79
+ describe 'event' do
80
+ before do
81
+ class AState < Aquam::State; end
82
+ class BState < Aquam::State; end
83
+
84
+ class StateMachine < Aquam::Machine
85
+ state :a, AState
86
+ state :b, BState
87
+ end
88
+ end
89
+
90
+ after do
91
+ Object.send(:remove_const, :AState)
92
+ Object.send(:remove_const, :BState)
93
+ Object.send(:remove_const, :StateMachine)
94
+ end
95
+
96
+ it 'defines an event with transitions' do
97
+ assert_equal [], StateMachine.events.keys
98
+
99
+ StateMachine.event :toggle do
100
+ transition from: :a, to: :b
101
+ transition from: :b, to: :a
102
+ end
103
+
104
+ assert_equal [:toggle], StateMachine.events.keys
105
+ end
106
+ end
107
+
108
+ describe 'transition' do
109
+ before do
110
+ class AState < Aquam::State; end
111
+ class BState < Aquam::State; end
112
+
113
+ class StateMachine < Aquam::Machine
114
+ state :a, AState
115
+ state :b, BState
116
+ end
117
+ end
118
+
119
+ after do
120
+ Object.send(:remove_const, :AState)
121
+ Object.send(:remove_const, :BState)
122
+ Object.send(:remove_const, :StateMachine)
123
+ end
124
+
125
+ it 'fails defining a transition between invalid states' do
126
+ assert_raises Aquam::InvalidStateError do
127
+ StateMachine.transition :undefined, :b, :event
128
+ end
129
+
130
+ assert_raises Aquam::InvalidStateError do
131
+ StateMachine.transition :a, :undefined, :event
132
+ end
133
+ end
134
+
135
+ it 'defines a new transition' do
136
+ assert_equal Hash.new, StateMachine.events[:event]
137
+
138
+ StateMachine.transition :a, :b, :event
139
+ expected_transition = { a: :b }
140
+
141
+ assert_equal expected_transition, StateMachine.events[:event]
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,68 @@
1
+ require_relative 'helper'
2
+ require_relative 'door_state_machine'
3
+
4
+ describe Aquam::Machine do
5
+ before do
6
+ @door = Door.new
7
+ @machine = @door.machine
8
+ end
9
+
10
+ describe 'valid_*? methods' do
11
+ it 'returns a boolean if is a valid state' do
12
+ assert @machine.valid_state?
13
+
14
+ @door.state = :wrong_state
15
+
16
+ deny @machine.valid_state?
17
+ end
18
+
19
+ it 'returns a boolean if is a valid event' do
20
+ assert @machine.valid_event? :open
21
+ deny @machine.valid_event? :lock
22
+ end
23
+
24
+ it 'returns a boolean if is a valid transition from the current state' do
25
+ deny @machine.valid_transition? :close
26
+ assert @machine.valid_transition? :open
27
+ end
28
+ end
29
+
30
+ describe 'current_state' do
31
+ it 'returns an instance of the current state object' do
32
+ assert @machine.current_state.instance_of? ClosedDoorState
33
+
34
+ @door.state = :opened
35
+
36
+ assert @machine.current_state.instance_of? OpenedDoorState
37
+ end
38
+
39
+ it 'fails if the current state was not defined into the machine' do
40
+ assert_raises Aquam::InvalidStateError do
41
+ @door.state = :must_fail
42
+ @machine.current_state
43
+ end
44
+ end
45
+ end
46
+
47
+ describe 'trigger' do
48
+ it 'fires the event and returns the new state' do
49
+ assert @machine.current_state.instance_of? ClosedDoorState
50
+
51
+ new_state = @machine.trigger(:open)
52
+
53
+ assert new_state.instance_of? OpenedDoorState
54
+ end
55
+
56
+ it 'fails if is not a valid event' do
57
+ assert_raises Aquam::InvalidEventError do
58
+ @machine.trigger :lock
59
+ end
60
+ end
61
+
62
+ it 'fails if is not a valid transition' do
63
+ assert_raises Aquam::InvalidTransitionError do
64
+ @machine.trigger :close
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,82 @@
1
+ require_relative 'helper'
2
+ require_relative 'door_state_machine'
3
+
4
+ describe Aquam::State do
5
+ after do
6
+ DoorState.class_variable_set :@@state_machine, nil
7
+ end
8
+
9
+ describe 'state_machine class method' do
10
+ it 'fails if it is not a valid Aquam::Machine class' do
11
+ assert_raises Aquam::InvalidStateMachineError do
12
+ DoorState.state_machine String
13
+ end
14
+ end
15
+
16
+ it 'defines the state machine that will be used in the entire hierarchy' do
17
+ assert_equal nil, DoorState.state_machine
18
+
19
+ DoorState.state_machine DoorStateMachine
20
+
21
+ assert_equal DoorStateMachine, DoorState.state_machine
22
+ end
23
+
24
+ it 'defines the state machine for every sublcass' do
25
+ class OpenedDoorState < DoorState; end
26
+
27
+ DoorState.state_machine DoorStateMachine
28
+
29
+ assert_equal DoorStateMachine, OpenedDoorState.state_machine
30
+ assert_equal DoorStateMachine, OpenedDoorState.new(nil).state_machine
31
+ end
32
+
33
+ it 'defines the state machine only once' do
34
+ class WindowStateMachine < Aquam::Machine; end
35
+
36
+ DoorState.state_machine DoorStateMachine
37
+ DoorState.state_machine WindowStateMachine
38
+
39
+ assert_equal DoorStateMachine, DoorState.state_machine
40
+
41
+ Object.send(:remove_const, :WindowStateMachine)
42
+ end
43
+
44
+ it 'defines all the events as methods' do
45
+ DoorState.state_machine DoorStateMachine
46
+
47
+ assert DoorState.instance_methods.include? :open
48
+ assert DoorState.instance_methods.include? :close
49
+ assert DoorState.instance_methods.include? :knock
50
+ end
51
+
52
+ it 'fails by default on every event method' do
53
+ DoorState.state_machine DoorStateMachine
54
+
55
+ assert_raises Aquam::InvalidTransitionError do
56
+ DoorState.new(nil).open
57
+ end
58
+
59
+ assert_raises Aquam::InvalidTransitionError do
60
+ DoorState.new(nil).close
61
+ end
62
+
63
+ assert_raises Aquam::InvalidTransitionError do
64
+ DoorState.new(nil).knock
65
+ end
66
+ end
67
+ end
68
+
69
+ describe 'state_machine instance method' do
70
+ it 'returns the state machine class defined' do
71
+ DoorState.state_machine DoorStateMachine
72
+
73
+ assert_equal DoorStateMachine, DoorState.new(nil).state_machine
74
+ end
75
+
76
+ it 'fails if the state machine was not defined' do
77
+ assert_raises Aquam::InvalidStateMachineError do
78
+ DoorState.new(nil).state_machine
79
+ end
80
+ end
81
+ end
82
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aquam
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Emiliano Mancuso
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Aquam adds a DSL and validations to define a State Machine
14
+ email:
15
+ - emiliano.mancuso@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - aquam.gemspec
22
+ - lib/aquam.rb
23
+ - lib/aquam/errors.rb
24
+ - lib/aquam/event_transitions.rb
25
+ - lib/aquam/machine.rb
26
+ - lib/aquam/machine_class_methods.rb
27
+ - lib/aquam/state.rb
28
+ - rakefile
29
+ - test/door_state_machine.rb
30
+ - test/helper.rb
31
+ - test/machine_class_methods_test.rb
32
+ - test/machine_test.rb
33
+ - test/state_test.rb
34
+ homepage: http://github.com/emancu/aquam
35
+ licenses:
36
+ - MIT
37
+ metadata: {}
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 2.4.5
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: DSL to define State Machines
58
+ test_files:
59
+ - test/door_state_machine.rb
60
+ - test/helper.rb
61
+ - test/machine_class_methods_test.rb
62
+ - test/machine_test.rb
63
+ - test/state_test.rb