aquam 0.0.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.
@@ -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