goalkeeper 0.0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e12557dafed1d9031a7a11e69d2c4acd39665cf8
4
- data.tar.gz: 87d0150ba4b4b3c6f72d6577308f3dbbb60d30b3
3
+ metadata.gz: 151b87201e8fbaa657d6aad6ef7aed33f8bf328a
4
+ data.tar.gz: 5a2b70b08cc0aa919d13cd3283300240d1895cdc
5
5
  SHA512:
6
- metadata.gz: 5053c93b993f256c6b91000713ed079058a0b1179cd6ca58e0aeefb5e6ec8dd69188ba091ed8bb5e23f1dd8fdede9e72ba348e01c408a9066521da81e08c5fa0
7
- data.tar.gz: 2eafd8b2593bd2f4ce935089fceb606f2ee954a412d771558ce349f5a00c56aebfe7d92f2d7d1d0036326ec13ed6862ffba10a91004dad9d0ad9d20310ddc747
6
+ metadata.gz: 2bb9d5d7819f6f778525ce4d1fbb638ac986f4a3644b26c306e07292396d8207adbb9499421fd33ce5f17034f03dc71c97cb3e893be537b3e761af16da1b3270
7
+ data.tar.gz: 55e43ca0fe7926ef24984da40b29f82af3dd8e9ff48d111b01e932e130a73dc9379ec16b59044d2802b6d1bdaf3bdbd016fde60bef2863cb0f65f8ac85c4dccd
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 John Weir, Pharos Enterprise Intelligence LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
1
  require "bundler/gem_tasks"
2
2
  require 'rake/testtask'
3
3
 
4
+ task default: [:test]
5
+
4
6
  Rake::TestTask.new do |t|
5
7
  t.libs << 'test'
6
8
  t.pattern = "test/*_test.rb"
data/goalkeeper.gemspec CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["John Weir"]
10
10
  spec.email = ["john@smokinggun.com"]
11
11
  spec.summary = %q{A Todo App for your application.}
12
- spec.description = %q{Goalkeeper is a system for validation that specific goals have been met by an application.}
13
- spec.homepage = ""
12
+ spec.description = %q{Goalkeeper is a system for verifing if specific goals have been met by an application.}
13
+ spec.homepage = "https://github.com/jweir/goalkeeper"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
data/lib/goalkeeper.rb CHANGED
@@ -1,17 +1,101 @@
1
1
  require "goalkeeper/version"
2
2
  require 'forwardable'
3
3
  require 'redis'
4
-
4
+ require 'time' # for Time.parse
5
+
6
+ # Goalkeeper provides methods to track if specific events(Goals) have been completed(met).
7
+ #
8
+ # It is not a complicated system and it is easy enough to roll your own. This
9
+ # is an extraction from a system Pharos EI has been using.
10
+ #
11
+ # A Goal is just a unique string. It is up to your application to
12
+ # define any schema for the Goal's label.
13
+ #
14
+ # For example you might have your Goals labeled by date and company id:
15
+ # "job:2016-01-17:company:7"
16
+ #
17
+ # When a Goal is met a record is created in Redis with a timestamp, this is the only
18
+ # persistent layer.
19
+ # Goalkeeper.met!("jobkey")
20
+ # # or
21
+ # Goalkeeper::Goal.new("jobkey").met!
22
+ #
23
+ # To check if a Goal as been met
24
+ # Goalkeeper::Goal.new("jobkey").met?
25
+ #
26
+ # Customize the redis client by setting it in your application
27
+ # Goalkeeper.redis = your_redis_client
28
+ #
29
+ # Each record has a default expiration of 24 hours, but this can be modified.
30
+ # Goalkeeper.expiration = number_of_seconds
31
+ #
32
+ # Redis keys are stored under the default namespace of "Goalkeeper:". The namespace can be configured:
33
+ #
34
+ # Goalkeeper.namespace = string
35
+ #
5
36
  class Goalkeeper
6
37
 
38
+ # Set the Redis client to a non default setting
39
+ def self.redis=(redis)
40
+ @redis = redis
41
+ end
42
+
43
+ def self.redis
44
+ @redis ||= Redis.new
45
+ end
46
+
7
47
  # Creates a persistent Goal market with the given label.
8
48
  def self.met!(label)
9
49
  Goal.new(label).met!
10
50
  end
11
51
 
52
+ # The TTL set for each met Goal record created in Redis
53
+ # Default is 24 hours
54
+ def self.expiration
55
+ @expiration ||= 24 * 60 * 60
56
+ end
57
+
58
+ # Overwrite the default expiration
59
+ def self.expiration=(number_of_seconds)
60
+ @expiration = number_of_seconds
61
+ end
62
+
63
+ def self.namespace
64
+ @namespace ||= "Goalkeeper"
65
+ end
66
+
67
+ def self.namespace=(ns)
68
+ @namespace = ns
69
+ end
70
+
71
+ # List is a collection of Goals to simplify tracking multiple goals.
72
+ #
73
+ # Create a new list
74
+ # mylist = Goalkeeper::List.new
75
+ #
76
+ # Add Goals you want to check for completion
77
+ # mylist.add("job1").add("job2")
78
+ # mylist.size
79
+ # #=> 2
80
+ #
81
+ # Check if all the goals are completed
82
+ # mylist.met?
83
+ # #=> false
84
+ #
85
+ # Get the unmet Goals
86
+ # mylist.unmet
87
+ # #=> [...]
88
+ #
89
+ # Get the met Goals
90
+ # mylist.met
91
+ # #=> [...]
92
+ #
93
+ # Iterate all Goals
94
+ # myslist.each {|goal| ...}
95
+ # myslist.map {|goal| ...}
12
96
  class List
13
97
  extend Forwardable
14
- def_delegators :@list, :size, :[]
98
+ def_delegators :@list, :size, :[], :each, :map
15
99
 
16
100
  def initialize
17
101
  @list = []
@@ -24,6 +108,7 @@ class Goalkeeper
24
108
  self
25
109
  end
26
110
 
111
+ # met? returns true if all Goals in the set have been met.
27
112
  def met?
28
113
  unmet.empty?
29
114
  end
@@ -38,61 +123,62 @@ class Goalkeeper
38
123
  end
39
124
 
40
125
  class Goal
126
+ # The unique label to identify this Goal
41
127
  attr_reader :label
128
+
129
+ # An optional object refrence which allows an application author to
130
+ # associate this goal to an object. The +ref+ is not used by Goalkeeper.
42
131
  attr_reader :ref
43
132
 
44
- # +label+ is a unique string to identify this demand.
133
+ # the TTL value for the Redis record. Defalts to Goalkeeper.expiration
134
+ attr_reader :expiration
135
+
136
+ # +label+ is a unique string to identify this Goal.
45
137
  # There is no checking if it is truly unique.
46
138
  #
47
139
  # +ref+ is an optional reference to any object. This
48
140
  # would be used by the end user's application.
49
- def initialize(label, ref: nil)
141
+ #
142
+ # +expiration+ can be set to override the gobal expiratin.
143
+ def initialize(label, ref: nil, expiration: Goalkeeper.expiration)
50
144
  @label = label
51
145
  @ref = ref
146
+ @expiration = expiration
52
147
  end
53
148
 
54
149
  def met!
55
- Store.write(self.label)
150
+ write
56
151
  self
57
152
  end
58
153
 
59
154
  def met?
60
- ! Store.read(self.label).nil?
155
+ ! read.nil?
61
156
  end
62
- end
63
-
64
- class Store
65
- EXPIRATION = 60 * 60 * 24 # 1 day
66
157
 
67
- def self.write(label)
68
- nl = ns(label)
69
- client.set nl, Time.now
70
- client.expire nl, EXPIRATION
158
+ # Time the goal was completed.
159
+ # WARNING retuns nil if the job is not met
160
+ def met_at
161
+ if met?
162
+ Time.parse(read)
163
+ else
164
+ nil
165
+ end
71
166
  end
72
167
 
73
- def self.read(label)
74
- nl = ns(label)
75
- client.get nl
76
- end
77
-
78
- def self.remove(label)
79
- nl = ns(label)
80
- client.del nl
168
+ # a namespaced key for the goal
169
+ def key
170
+ "#{Goalkeeper.namespace}:#{label}"
81
171
  end
82
172
 
83
173
  protected
84
174
 
85
- def self.ns(label)
86
- namespace + ":"+ label
175
+ def write
176
+ Goalkeeper.redis.set(self.key, Time.now)
177
+ Goalkeeper.redis.expire(self.key, self.expiration)
87
178
  end
88
179
 
89
- def self.namespace
90
- "Goalkeeper"
180
+ def read
181
+ Goalkeeper.redis.get self.key
91
182
  end
92
-
93
- def self.client
94
- @client ||= Redis.new
95
- end
96
-
97
183
  end
98
184
  end
@@ -1,3 +1,3 @@
1
1
  class Goalkeeper
2
- VERSION = "0.0.1"
2
+ VERSION = "0.2"
3
3
  end
@@ -1,23 +1,33 @@
1
1
  require 'test_helper'
2
2
 
3
- # API
4
- # Goal.configure
5
- # Goal.met label, ttl
6
- # Goal::List.new
7
- # .add
8
- # .met
9
- # .unmet
10
- # .met?
11
- #
12
- # configure
13
- # redis client
14
- # namespace
15
- #
16
- # Goal -> score, completed, met
17
- #
18
- # was a Goal met?
19
- # when was it met?
20
3
  describe Goalkeeper do
4
+ before do
5
+ Goalkeeper.redis.flushdb
6
+ end
7
+
8
+ describe "::redis" do
9
+ it "returns the Redis client" do
10
+ assert Goalkeeper.redis.is_a?(Redis)
11
+ end
12
+ end
13
+
14
+ describe "::namespace" do
15
+ it "defaults to Goalkeeper" do
16
+ assert_equal "Goalkeeper", Goalkeeper.namespace
17
+ end
18
+
19
+ it "can be user defined" do
20
+ ns = Goalkeeper.namespace
21
+
22
+ Goalkeeper.namespace = "NewNamespace"
23
+ assert_equal "NewNamespace", Goalkeeper.namespace
24
+ goal = Goalkeeper::Goal.new("x")
25
+ assert_equal "NewNamespace:x", goal.key
26
+
27
+ # reset
28
+ Goalkeeper.namespace = ns
29
+ end
30
+ end
21
31
 
22
32
  describe Goalkeeper::List do
23
33
  before do
@@ -25,7 +35,7 @@ describe Goalkeeper do
25
35
  end
26
36
 
27
37
  describe "#add" do
28
- it "create a Goal" do
38
+ it "creates a Goal" do
29
39
  @goals.add("a:1")
30
40
  assert_equal 1, @goals.size
31
41
  assert_equal "a:1", @goals[0].label
@@ -42,61 +52,94 @@ describe Goalkeeper do
42
52
  end
43
53
  end
44
54
 
45
- describe "#met" do
46
- it "returns all Goals which have been met"
47
- end
55
+ describe "with goals" do
56
+ before do
57
+ @goals.add("x").add("y")
58
+ end
48
59
 
49
- describe "#unmet" do
50
- it "returns all Goals which have not been met"
51
- end
60
+ describe "#met" do
61
+ it "returns all Goals which have been met" do
62
+ assert @goals.met.empty?
63
+ @goals[0].met!
64
+ assert_equal ["x"], @goals.met.map(&:label)
65
+ @goals[1].met!
66
+ assert_equal ["x","y"], @goals.met.map(&:label)
67
+ end
68
+ end
52
69
 
53
- describe "#met?" do
54
- it "is true when all Goals have been met"
55
- end
56
- end
70
+ describe "#unmet" do
71
+ it "returns all Goals which have not been met" do
72
+ assert_equal ["x","y"], @goals.unmet.map(&:label)
73
+ @goals[0].met!
74
+ assert_equal ["y"], @goals.unmet.map(&:label)
75
+ @goals[1].met!
76
+ assert @goals.unmet.empty?
77
+ end
78
+ end
57
79
 
58
- describe "met!" do
59
- it "should create a Goal for the given label" do
60
- assert Goalkeeper.met!("x:1")
80
+ describe "#met?" do
81
+ it "is true when all Goals have been met" do
82
+ assert ! @goals.met?
83
+ @goals.each(&:met!)
84
+ assert @goals.met?
85
+ end
86
+ end
61
87
  end
62
-
63
- it "has a default ttl expiration"
64
- it "takes an optional at: timestamp"
65
- it "takes an optional ttl for expiration"
66
88
  end
67
89
 
68
- describe "namespace" do
69
- end
90
+ describe Goalkeeper::Goal do
91
+ before do
92
+ @goal = Goalkeeper::Goal.new("b")
93
+ end
70
94
 
71
- describe "configuation" do
72
- # allow setting the redis client
73
- end
74
- end
95
+ it "has a label" do
96
+ assert_equal "b", @goal.label
97
+ end
75
98
 
76
- describe "Integration" do
77
- before do
78
- puts "fix this!"
79
- Redis.new.flushdb
80
- end
99
+ it "has a namespaced key" do
100
+ assert_equal "Goalkeeper:b", @goal.key
101
+ end
81
102
 
82
- it "works like this" do
83
- Goalkeeper.met! "x"
103
+ it "is met? if the label has a Redis record" do
104
+ assert ! @goal.met?
105
+ Goalkeeper.redis.set @goal.key, Time.now
106
+ assert @goal.met?
107
+ end
84
108
 
85
- d = Goalkeeper::List
86
- .new
87
- .add("x")
88
- .add("y")
109
+ describe "met_at" do
110
+ it "is nil if the Goal is not met" do
111
+ assert_equal nil, @goal.met_at
112
+ end
89
113
 
90
- assert_equal false, d.met?
114
+ it "is the timestamp that the Goal was met" do
115
+ @t = Time.parse(Time.now.to_s)
116
+ @goal.met!
117
+ assert_equal @t, @goal.met_at
118
+ end
119
+ end
91
120
 
92
- assert_equal ["y"], d.unmet.map(&:label)
93
- assert_equal ["x"], d.met.map(&:label)
121
+ describe "#met!" do
122
+ it "creates a Redis record" do
123
+ assert Goalkeeper.redis.get(@goal.key).nil?
124
+ @goal.met!
125
+ assert ! Goalkeeper.redis.get(@goal.key).nil?
126
+ end
94
127
 
95
- Goalkeeper.met! "y"
128
+ it "has a default ttl expiration" do
129
+ @goal.met!
130
+ assert_equal @goal.expiration, Goalkeeper.redis.ttl(@goal.key)
131
+ end
132
+ end
96
133
 
97
- assert_equal true, d.met?
134
+ describe "#expiration" do
135
+ it "has a default of 24 hours" do
136
+ assert_equal 24 * 60 * 60, @goal.expiration
137
+ end
98
138
 
99
- assert_equal ["x","y"], d.met.map(&:label)
139
+ it "can be set at initialization" do
140
+ g = Goalkeeper::Goal.new("x", expiration: 60)
141
+ assert_equal 60, g.expiration
142
+ end
143
+ end
100
144
  end
101
145
  end
102
-
@@ -0,0 +1,136 @@
1
+ # RedisInstance is copied from
2
+ # https://github.com/resque/resque-scheduler/blob/master/test/support/redis_instance.rb
3
+ # released under an MIT license
4
+ # modifications have been made
5
+ require 'socket'
6
+ require 'timeout'
7
+ require 'fileutils'
8
+
9
+ class RedisInstance
10
+ class << self
11
+ @running = false
12
+ @port = nil
13
+ @pid = nil
14
+ @waiting = false
15
+
16
+ def run_if_needed!
17
+ run! unless @running
18
+ end
19
+
20
+ def run!
21
+ ensure_redis_server_present!
22
+ ensure_pid_directory
23
+ start_redis_server
24
+ post_boot_waiting_and_such
25
+
26
+ @running = true
27
+ client
28
+ end
29
+
30
+ def stop!
31
+ $stdout.puts "Sending TERM to Redis (#{pid})..." if $stdout.tty?
32
+ Process.kill('TERM', pid)
33
+
34
+ @port = nil
35
+ @running = false
36
+ @pid = nil
37
+ end
38
+
39
+ def client
40
+ Redis.new(
41
+ hostname: '127.0.0.1', port: port, thread_safe: true
42
+ )
43
+ end
44
+
45
+ private
46
+
47
+ def post_boot_waiting_and_such
48
+ wait_for_pid
49
+ puts "Booted isolated Redis on #{port} with PID #{pid}."
50
+
51
+ wait_for_redis_boot
52
+
53
+ # Ensure we tear down Redis on Ctrl+C / test failure.
54
+ at_exit { stop! }
55
+ end
56
+
57
+ def ensure_redis_server_present!
58
+ unless system('redis-server -v')
59
+ fail "** can't find `redis-server` in your path"
60
+ end
61
+ end
62
+
63
+ def wait_for_redis_boot
64
+ Timeout.timeout(10) do
65
+ loop do
66
+ begin
67
+ break if client.ping == "PONG"
68
+ rescue Redis::CannotConnectError
69
+ @waiting = true
70
+ end
71
+ end
72
+ @waiting = false
73
+ end
74
+ end
75
+
76
+ def ensure_pid_directory
77
+ FileUtils.mkdir_p(File.dirname(pid_file))
78
+ end
79
+
80
+ def start_redis_server
81
+ IO.popen('redis-server -', 'w+') do |server|
82
+ server.write(config)
83
+ server.close_write
84
+ end
85
+ end
86
+
87
+ def pid
88
+ @pid ||= File.read(pid_file).to_i
89
+ end
90
+
91
+ def wait_for_pid
92
+ Timeout.timeout(10) do
93
+ loop { break if File.exist?(pid_file) }
94
+ end
95
+ end
96
+
97
+ def port
98
+ @port ||= random_port
99
+ end
100
+
101
+ def pid_file
102
+ '/tmp/redis-scheduler-test.pid'
103
+ end
104
+
105
+ def config
106
+ <<-EOF
107
+ daemonize yes
108
+ pidfile #{pid_file}
109
+ port #{port}
110
+ EOF
111
+ end
112
+
113
+ # Returns a random port in the upper (10000-65535) range.
114
+ def random_port
115
+ ports = (10_000..65_535).to_a
116
+
117
+ loop do
118
+ port = ports[rand(ports.size)]
119
+ return port if port_available?('127.0.0.1', port)
120
+ end
121
+ end
122
+
123
+ def port_available?(ip, port, seconds = 1)
124
+ Timeout.timeout(seconds) do
125
+ begin
126
+ TCPSocket.new(ip, port).close
127
+ false
128
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
129
+ true
130
+ end
131
+ end
132
+ rescue Timeout::Error
133
+ true
134
+ end
135
+ end
136
+ end
data/test/test_helper.rb CHANGED
@@ -3,3 +3,9 @@ require 'minitest/autorun'
3
3
  require 'minitest/pride'
4
4
 
5
5
  require './lib/goalkeeper'
6
+
7
+ require './test/support/redis_instance'
8
+
9
+ # Start up Redis on a random port
10
+ Goalkeeper.redis = RedisInstance.run!
11
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: goalkeeper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: '0.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Weir
@@ -52,8 +52,8 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '10.0'
55
- description: Goalkeeper is a system for validation that specific goals have been met
56
- by an application.
55
+ description: Goalkeeper is a system for verifing if specific goals have been met by
56
+ an application.
57
57
  email:
58
58
  - john@smokinggun.com
59
59
  executables: []
@@ -62,6 +62,7 @@ extra_rdoc_files: []
62
62
  files:
63
63
  - ".gitignore"
64
64
  - Gemfile
65
+ - LICENSE
65
66
  - LICENSE.txt
66
67
  - README.md
67
68
  - Rakefile
@@ -69,8 +70,9 @@ files:
69
70
  - lib/goalkeeper.rb
70
71
  - lib/goalkeeper/version.rb
71
72
  - test/goalkeeper_test.rb
73
+ - test/support/redis_instance.rb
72
74
  - test/test_helper.rb
73
- homepage: ''
75
+ homepage: https://github.com/jweir/goalkeeper
74
76
  licenses:
75
77
  - MIT
76
78
  metadata: {}
@@ -96,4 +98,5 @@ specification_version: 4
96
98
  summary: A Todo App for your application.
97
99
  test_files:
98
100
  - test/goalkeeper_test.rb
101
+ - test/support/redis_instance.rb
99
102
  - test/test_helper.rb