czar 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +119 -0
- data/Rakefile +11 -0
- data/czar.gemspec +25 -0
- data/lib/czar/command.rb +53 -0
- data/lib/czar/version.rb +3 -0
- data/lib/czar.rb +5 -0
- data/test/czar/command_test.rb +151 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 471ebb54150c95923d569f6ce7d32072a17974e8
|
4
|
+
data.tar.gz: 4cf441e9b83e54c5cd5f558fa03018481f6f67a5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0362a5ee711c89fa76765e2585ada348990f7d9fa8ce30c5d478e20a4693153c8867d7cf72e0ee4df807ac686bc68feb6c213fcb1b3ba2d2a9d34439829c7893
|
7
|
+
data.tar.gz: fc23abaa9a1d2af589d2ab0515b43fd8a403b3cfb510ab9c00839752385ee38fd6d7187fb34793792b41123613e77613ff55514da08a1b6408964beddeca9416
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Rahoul Baruah
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# Czar
|
2
|
+
|
3
|
+
Czar is a framework for building applications around the Command
|
4
|
+
pattern.
|
5
|
+
|
6
|
+
Everything your application does is a Command of some kind; those
|
7
|
+
commands may be simple actions, sequences of actions, sequences with
|
8
|
+
decisions within them or series that trigger other commands.
|
9
|
+
|
10
|
+
The advantage of using commands within, for example, a Rails application
|
11
|
+
is that you can then make your database model classes *passive* - that
|
12
|
+
is they become purely data - and the interesting stuff is all handled by
|
13
|
+
commands. No more fat models with callbacks all over the place.
|
14
|
+
|
15
|
+
Plus using commands makes it easy to ensure that authorisation is done
|
16
|
+
correctly (as each command is well-defined) and makes it trivial to add
|
17
|
+
in logging and so on.
|
18
|
+
|
19
|
+
Czar can allow commands to be persisted (with a in-memory and a Redis adapter for now).
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
Add this line to your application's Gemfile:
|
24
|
+
|
25
|
+
gem 'czar'
|
26
|
+
|
27
|
+
And then execute:
|
28
|
+
|
29
|
+
$ bundle
|
30
|
+
|
31
|
+
Or install it yourself as:
|
32
|
+
|
33
|
+
$ gem install czar
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
The Command module represents an implementation of the Command pattern
|
38
|
+
where each Command has an internal state machine and can optionally spawn child commands.
|
39
|
+
At its simplest, a Command will be executed, moving it from "start" state to "complete" state.
|
40
|
+
For example:
|
41
|
+
```
|
42
|
+
class SimpleCommand
|
43
|
+
include Czar::Command
|
44
|
+
|
45
|
+
def start
|
46
|
+
result = some_complex_calculation
|
47
|
+
mark_as :complete, result: result
|
48
|
+
end
|
49
|
+
|
50
|
+
def some_complex_calculation
|
51
|
+
return :whatever
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
To use this, simply call SimpleCommand.new.execute - the command will perform #some_complex_calculation and then be marked as :complete
|
57
|
+
|
58
|
+
By itself, this is pretty boring. However, as we've got a simple state machine in there, we can do more interesting stuff; especially when a command is persistent.
|
59
|
+
|
60
|
+
For example:
|
61
|
+
|
62
|
+
class DrivesACar
|
63
|
+
include Czar::Command
|
64
|
+
|
65
|
+
def start
|
66
|
+
mark_as :moving
|
67
|
+
end
|
68
|
+
|
69
|
+
def moving
|
70
|
+
move_forward
|
71
|
+
if has_reached_destination?
|
72
|
+
mark_as :complete
|
73
|
+
elsif traffic_light.colour == :green
|
74
|
+
mark_as :driving
|
75
|
+
else
|
76
|
+
mark_as :stopped
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def move_forward
|
81
|
+
:vroom
|
82
|
+
end
|
83
|
+
|
84
|
+
def has_reached_destination?
|
85
|
+
# code goes here
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
In this case, we instantiate a DrivesACar command and store it somewhere. Every now and then (in response to a timer, a cron job or some other trigger) we call execute, which looks at the command's internal state and chooses if it is moving or stopped. Eventually, when we have reached our destination, the command is marked as complete. Also note, that as we do nothing when stopped, there's no need to define a stopped method.
|
90
|
+
|
91
|
+
Commands can also trigger child commands, and are notified when the child completes.
|
92
|
+
|
93
|
+
For example:
|
94
|
+
|
95
|
+
class CourierDeliversAParcel < Struct.new(:pickup_location, :dropoff_location)
|
96
|
+
include Czar::Command
|
97
|
+
|
98
|
+
def start
|
99
|
+
perform CollectsParcel.new(pickup_location)
|
100
|
+
mark_as :waiting_for_pickup
|
101
|
+
end
|
102
|
+
|
103
|
+
def child_task_completed task
|
104
|
+
if self.state == :waiting_for_pickup
|
105
|
+
perform DeliversParcel.new(dropoff_location)
|
106
|
+
mark_as :waiting_for_dropoff
|
107
|
+
elsif self.state == :waiting_for_dropoff
|
108
|
+
mark_as :complete
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
## Contributing
|
114
|
+
|
115
|
+
1. Fork it ( http://github.com/<my-github-username>/czar/fork )
|
116
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
117
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
118
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
119
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/czar.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'czar/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "czar"
|
8
|
+
spec.version = Czar::VERSION
|
9
|
+
spec.authors = ["Rahoul Baruah"]
|
10
|
+
spec.email = ["rahoul@3hv.co.uk"]
|
11
|
+
spec.summary = "A framework for implementing the Command pattern"
|
12
|
+
spec.description = "Persistent, hierarchical commands"
|
13
|
+
spec.homepage = "http://passiverecord.com"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "minitest"
|
24
|
+
spec.add_development_dependency "mocha"
|
25
|
+
end
|
data/lib/czar/command.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Czar
|
2
|
+
module Command
|
3
|
+
|
4
|
+
def execute
|
5
|
+
self.send self.state if self.respond_to? self.state
|
6
|
+
end
|
7
|
+
|
8
|
+
def result
|
9
|
+
internal[:result]
|
10
|
+
end
|
11
|
+
|
12
|
+
def completed?
|
13
|
+
state == :complete
|
14
|
+
end
|
15
|
+
|
16
|
+
def state
|
17
|
+
@state ||= :start
|
18
|
+
end
|
19
|
+
|
20
|
+
def children
|
21
|
+
@children ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
attr_accessor :parent
|
27
|
+
|
28
|
+
def perform child_task
|
29
|
+
child_task.parent = self
|
30
|
+
children << child_task
|
31
|
+
child_task.execute
|
32
|
+
end
|
33
|
+
|
34
|
+
def mark_as state, params = {}
|
35
|
+
@state = state.to_sym
|
36
|
+
@internal_state = params.dup
|
37
|
+
notify_parent if @state == :complete
|
38
|
+
end
|
39
|
+
|
40
|
+
def internal
|
41
|
+
@internal_state ||= {}
|
42
|
+
end
|
43
|
+
|
44
|
+
def notify_parent
|
45
|
+
parent.child_task_completed self unless parent.nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
def child_task_completed child_task
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
data/lib/czar/version.rb
ADDED
data/lib/czar.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
require_relative '../../lib/czar/command'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'mocha/setup'
|
4
|
+
|
5
|
+
describe Czar::Command do
|
6
|
+
|
7
|
+
describe "initialisation" do
|
8
|
+
subject { initialiser.new }
|
9
|
+
|
10
|
+
it "is ready to start" do
|
11
|
+
subject.state.must_equal :start
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:initialiser) do
|
15
|
+
Class.new do
|
16
|
+
include Czar::Command
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "performing a one stage task" do
|
22
|
+
subject { one_stage_task.new }
|
23
|
+
|
24
|
+
it "executes the start task" do
|
25
|
+
subject.execute
|
26
|
+
|
27
|
+
subject.completed?.must_equal true
|
28
|
+
subject.result.must_equal "DONE"
|
29
|
+
end
|
30
|
+
|
31
|
+
let(:one_stage_task) do
|
32
|
+
Class.new do
|
33
|
+
include Czar::Command
|
34
|
+
|
35
|
+
def start
|
36
|
+
mark_as :complete, result: "DONE"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "performing a multi stage task" do
|
43
|
+
subject { multi_stage_task.new }
|
44
|
+
|
45
|
+
it "executes the tasks and moves through the states in order" do
|
46
|
+
subject.execute
|
47
|
+
subject.state.must_equal :in_progress
|
48
|
+
|
49
|
+
subject.execute
|
50
|
+
subject.state.must_equal :having_a_rest
|
51
|
+
|
52
|
+
subject.execute
|
53
|
+
subject.state.must_equal :having_a_rest
|
54
|
+
|
55
|
+
subject.execute
|
56
|
+
subject.completed?.must_equal true
|
57
|
+
subject.result.must_equal 'Goodbye'
|
58
|
+
end
|
59
|
+
|
60
|
+
let(:multi_stage_task) do
|
61
|
+
Class.new do
|
62
|
+
include Czar::Command
|
63
|
+
|
64
|
+
def start
|
65
|
+
mark_as :in_progress
|
66
|
+
end
|
67
|
+
|
68
|
+
def in_progress
|
69
|
+
mark_as :having_a_rest, counter: 1
|
70
|
+
end
|
71
|
+
|
72
|
+
def having_a_rest
|
73
|
+
if internal[:counter] == 1
|
74
|
+
mark_as :having_a_rest, counter: 2
|
75
|
+
else
|
76
|
+
mark_as :complete, result: 'Goodbye'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "having states that do nothing" do
|
84
|
+
subject { do_nothing_task.new }
|
85
|
+
|
86
|
+
it "executes the first task and then does nothing" do
|
87
|
+
subject.execute
|
88
|
+
subject.state.must_equal :idle
|
89
|
+
subject.execute
|
90
|
+
subject.state.must_equal :idle
|
91
|
+
end
|
92
|
+
|
93
|
+
let(:do_nothing_task) do
|
94
|
+
Class.new do
|
95
|
+
include Czar::Command
|
96
|
+
|
97
|
+
def start
|
98
|
+
mark_as :idle
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "triggering child commands" do
|
105
|
+
subject { parent_task.new }
|
106
|
+
|
107
|
+
it "starts the child command and is not marked as complete until both it and the child have completed" do
|
108
|
+
subject.execute
|
109
|
+
|
110
|
+
child = subject.children.first
|
111
|
+
child.state.must_equal :in_progress
|
112
|
+
subject.state.must_equal :waiting_for_child_to_complete
|
113
|
+
subject.completed?.wont_equal true
|
114
|
+
|
115
|
+
child.execute
|
116
|
+
child.completed?.must_equal true
|
117
|
+
subject.completed?.must_equal true
|
118
|
+
end
|
119
|
+
|
120
|
+
let(:parent_task) do
|
121
|
+
Class.new do
|
122
|
+
include Czar::Command
|
123
|
+
|
124
|
+
def start
|
125
|
+
perform ChildTask.new
|
126
|
+
mark_as :waiting_for_child_to_complete
|
127
|
+
end
|
128
|
+
|
129
|
+
def waiting_for_child_to_complete
|
130
|
+
sleep 0.1
|
131
|
+
end
|
132
|
+
|
133
|
+
def child_task_completed child_task
|
134
|
+
mark_as :complete
|
135
|
+
end
|
136
|
+
|
137
|
+
class ChildTask
|
138
|
+
include Czar::Command
|
139
|
+
|
140
|
+
def start
|
141
|
+
mark_as :in_progress
|
142
|
+
end
|
143
|
+
|
144
|
+
def in_progress
|
145
|
+
mark_as :complete
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: czar
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rahoul Baruah
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mocha
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Persistent, hierarchical commands
|
70
|
+
email:
|
71
|
+
- rahoul@3hv.co.uk
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- .gitignore
|
77
|
+
- Gemfile
|
78
|
+
- LICENSE.txt
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- czar.gemspec
|
82
|
+
- lib/czar.rb
|
83
|
+
- lib/czar/command.rb
|
84
|
+
- lib/czar/version.rb
|
85
|
+
- test/czar/command_test.rb
|
86
|
+
homepage: http://passiverecord.com
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.2.1
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: A framework for implementing the Command pattern
|
110
|
+
test_files:
|
111
|
+
- test/czar/command_test.rb
|