state_validations 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +20 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +135 -0
- data/Rakefile +41 -0
- data/lib/active_model/validations/helper_methods.rb +24 -0
- data/lib/active_model/validations/state_history_entry_validator.rb +40 -0
- data/lib/active_model/validations/state_history_validator.rb +118 -0
- data/lib/state_validations.rb +6 -0
- data/state_validations.gemspec +28 -0
- data/test/test_helper.rb +8 -0
- data/test/unit/helper_methods_test.rb +45 -0
- data/test/unit/state_history_entry_validator_test.rb +101 -0
- data/test/unit/state_history_validator_test.rb +308 -0
- metadata +171 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 PagerDuty, Inc.
|
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,135 @@
|
|
1
|
+
# StateValidations
|
2
|
+
|
3
|
+
**Current version:** 0.2.0
|
4
|
+
|
5
|
+
The StateValidations gem is a collection of two Validators that aid in
|
6
|
+
verifying the integrity of a state history association. As defined here, a
|
7
|
+
state history is a collection of entries, each with start and end time
|
8
|
+
attributes.
|
9
|
+
|
10
|
+
One validator provided is the aptly-named StateHistoryValidator. This Validator
|
11
|
+
examines a state history holistically checking for a variety of issues. These
|
12
|
+
issues include:
|
13
|
+
|
14
|
+
* Overlaps: the time ranges of two consecutive entries overlap
|
15
|
+
* Gaps: the time ranges of two consecutive entries are not continuous
|
16
|
+
* Self-transitions: two continuous consecutive entries are equivalent in state
|
17
|
+
* Simultaneous states: an entry that is not the last entry has not ended (i.e.
|
18
|
+
the entry's end time is `nil`)
|
19
|
+
* Ended state histories: the last entry has ended (i.e. last entry's end time
|
20
|
+
is `nil`)
|
21
|
+
|
22
|
+
The user of this validator can disable any combination of these checks as he or
|
23
|
+
she wishes through the `:allow` option.
|
24
|
+
|
25
|
+
The other validator provided is the StateHistoryEntryValidator. This Validator
|
26
|
+
examines each state history entry on its own, checking for these issues:
|
27
|
+
|
28
|
+
* Zero-length entries: the entry begins and ends at the same time
|
29
|
+
* Ends before starting: the entry ends before it starts (end < start)
|
30
|
+
|
31
|
+
As with the first validator, the user can disable any combination of these
|
32
|
+
checks.
|
33
|
+
|
34
|
+
## Installation
|
35
|
+
|
36
|
+
Add this line to your application's Gemfile:
|
37
|
+
|
38
|
+
gem 'state_validations'
|
39
|
+
|
40
|
+
And then execute:
|
41
|
+
|
42
|
+
$ bundle
|
43
|
+
|
44
|
+
Or install it yourself as:
|
45
|
+
|
46
|
+
$ git clone git://github.com/PagerDuty/state-history-validator.git
|
47
|
+
$ rake build
|
48
|
+
$ gem install pkg/state_validations-(version).gem
|
49
|
+
|
50
|
+
## Usage
|
51
|
+
|
52
|
+
### Examples
|
53
|
+
|
54
|
+
#### StateHistoryValidator
|
55
|
+
Let's say that we want to check the integrity of `:state_history_entries`.
|
56
|
+
This is how we might call the validator:
|
57
|
+
|
58
|
+
validate_state_history :state_history_entries,
|
59
|
+
:self_transition_match => [:state_id],
|
60
|
+
:allow => [:gaps]
|
61
|
+
|
62
|
+
Here, our validator will allow for gaps in the state history entries. It
|
63
|
+
will look for self-transitions depending on whether the `:state_id`
|
64
|
+
attribute of two consecutive entries match.
|
65
|
+
|
66
|
+
#### StateHistoryEntryValidator
|
67
|
+
Let's say we want to validate an entry, but we don't particularly care
|
68
|
+
about zero-entries. In addition, our entry uses the attribute `:begin` to
|
69
|
+
indicate its start time attribute. This is how we might use the validator:
|
70
|
+
|
71
|
+
validate_state_history_entry
|
72
|
+
:start => :begin
|
73
|
+
:allow => [:zero_duration]
|
74
|
+
|
75
|
+
Note that we omitted the comma after `validate_state_history_entry`.
|
76
|
+
|
77
|
+
The StateHistoryEntryValidator requires that the receiving class validate
|
78
|
+
for the presence of the `:start` attribute. The `validate_state_history_entry`
|
79
|
+
construct, which is the recommended way to call this validator, performs this
|
80
|
+
validation automatically.
|
81
|
+
|
82
|
+
### Interface
|
83
|
+
With these examples in mind, here is the full documentation on every option
|
84
|
+
that can be toggled.
|
85
|
+
|
86
|
+
#### StateHistoryValidator
|
87
|
+
* `:start` -- (default = `:start`) Specifies the attribute referring to the
|
88
|
+
starting time of each entry
|
89
|
+
* `:end` -- (default = `:end`) Specifies the attribute referring to the
|
90
|
+
ending time of each entry
|
91
|
+
* `:order` -- (default = `#{start} ASC, ISNULL(#{end}) ASC, #{end} ASC`)
|
92
|
+
Specifies the SQL order by which the validator will sort all entries in the
|
93
|
+
state history. This is essential in ensuring that the validator obtains the
|
94
|
+
correct chronological order of entries so that it can properly check for
|
95
|
+
issues. By default, the validator sorts in ascending order of starting
|
96
|
+
time. If the start times for two entries are equal, it will then sort by
|
97
|
+
end times (pushing `nil` ends to the bottom).
|
98
|
+
* `:self_transition_match` -- (default = `[]`) Specifies the attributes to
|
99
|
+
check two states for equality. This is used when checking for self-transitions.
|
100
|
+
If self-transitions are allowed, this option has no effect. If this option
|
101
|
+
is left to its default value of an empty array, the validator will not check
|
102
|
+
for self-transitions.
|
103
|
+
* `:allow` -- (default = `[]`) Specifies issues to be ignored by the validator.
|
104
|
+
This is specified as a list of issues. By default, all checks are performed.
|
105
|
+
The issues that can be specified are:
|
106
|
+
* `:gaps` -- Ignore gaps
|
107
|
+
* `:overlaps` -- Ignore overlaps
|
108
|
+
* `:self_transitions` -- Ignore two consecutive equal states. As a side
|
109
|
+
note, two equal states separated by a gap are not considered to be in
|
110
|
+
self-transition.
|
111
|
+
* `:active_middle_states` -- Ignore the presence of an active state in the
|
112
|
+
middle of a state history. An active state is defined by the state's `:end`
|
113
|
+
attribute being `nil`.
|
114
|
+
This excludes the final state.
|
115
|
+
* `:inactive_end_state` -- Ignore the lack of a final active ending state.
|
116
|
+
Using our earlier definition, this means that the last entry does not have
|
117
|
+
`nil` as the value of its `:end` attribute.
|
118
|
+
|
119
|
+
#### StateHistoryEntryValidator
|
120
|
+
* `:start` -- (default = `:start`) Specifies the attribute referring to the
|
121
|
+
starting time of this entry
|
122
|
+
* `:end` -- (default = `:end`) Specifies the attribute referring to the ending
|
123
|
+
time of this entry
|
124
|
+
* `:allow` -- (default = `[]`) Specifies issues to be ignored by the validator.
|
125
|
+
* `:zero_duration` -- Ignore start time equal to end time
|
126
|
+
* `:end_before_start` -- Ignore end time less than start time
|
127
|
+
|
128
|
+
|
129
|
+
## Contributing
|
130
|
+
|
131
|
+
1. Fork it
|
132
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
133
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
134
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
135
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
desc 'Run shoulda unit tests'
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << 'lib'
|
8
|
+
t.libs << 'test'
|
9
|
+
t.pattern = 'test/**/*_test.rb'
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
# Thanks, jekyll, for having your Rakefile on Github
|
15
|
+
|
16
|
+
def name
|
17
|
+
@name ||= Dir['*.gemspec'].first.split('.').first
|
18
|
+
end
|
19
|
+
|
20
|
+
def version
|
21
|
+
line = File.read(gemspec_file)[/^\s*gem\.version\s*=\s*.*/]
|
22
|
+
line.match(/.*gem\.version\s*=\s*['"](.*)['"]/)[1]
|
23
|
+
end
|
24
|
+
|
25
|
+
def gemspec_file
|
26
|
+
"#{name}.gemspec"
|
27
|
+
end
|
28
|
+
|
29
|
+
def gem_file
|
30
|
+
"#{name}-#{version}.gem"
|
31
|
+
end
|
32
|
+
|
33
|
+
task :build do
|
34
|
+
sh "mkdir -p pkg"
|
35
|
+
sh "gem build #{gemspec_file}"
|
36
|
+
sh "mv #{gem_file} pkg"
|
37
|
+
end
|
38
|
+
|
39
|
+
task :push do
|
40
|
+
sh "gem push pkg/#{gem_file}"
|
41
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'active_model/validations'
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module Validations
|
5
|
+
|
6
|
+
module HelperMethods
|
7
|
+
def validate_state_history_entry(options = {})
|
8
|
+
start_attr = options[:start] || :start
|
9
|
+
validates start_attr, :presence => true
|
10
|
+
|
11
|
+
validates_with StateHistoryEntryValidator, options
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def validate_state_history(association, options = {})
|
16
|
+
params = { :association => association }
|
17
|
+
params.merge!(options)
|
18
|
+
validates_with StateHistoryValidator, params
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ActiveModel
|
2
|
+
module Validations
|
3
|
+
|
4
|
+
class StateHistoryEntryValidator < ActiveModel::Validator
|
5
|
+
def initialize(options = {})
|
6
|
+
super
|
7
|
+
|
8
|
+
@start = options[:start] || :start
|
9
|
+
@end = options[:end] || :end
|
10
|
+
@allow = options[:allow] || []
|
11
|
+
end
|
12
|
+
|
13
|
+
def validate(record)
|
14
|
+
non_zero?(record) unless @allow.include? :zero_duration
|
15
|
+
start_before_end?(record) unless @allow.include? :end_before_start
|
16
|
+
|
17
|
+
return
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def non_zero?(record)
|
23
|
+
start = record[@start]
|
24
|
+
_end = record[@end]
|
25
|
+
if start == _end
|
26
|
+
record.errors[:base] << "Start time (#{start}) is equal to end (#{_end})"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def start_before_end?(record)
|
31
|
+
start = record[@start]
|
32
|
+
_end = record[@end]
|
33
|
+
if !_end.blank? && start > _end
|
34
|
+
record.errors[:base] << "Start time (#{start}) is later than end time (#{_end})"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module ActiveModel
|
2
|
+
module Validations
|
3
|
+
|
4
|
+
class StateHistoryValidator < ActiveModel::Validator
|
5
|
+
def initialize(options = {})
|
6
|
+
super
|
7
|
+
|
8
|
+
@association = options[:association]
|
9
|
+
|
10
|
+
@start = options[:start] || :start
|
11
|
+
@end = options[:end] || :end
|
12
|
+
@order = options[:order] || "#{@start.to_s} ASC, ISNULL(#{@end}) ASC, #{@end} ASC"
|
13
|
+
@allow = options[:allow] || []
|
14
|
+
@match = options[:self_transition_match] || []
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate(record)
|
18
|
+
entries = record.send(@association)
|
19
|
+
sorted_history = entries.order(@order)
|
20
|
+
|
21
|
+
return if sorted_history.blank?
|
22
|
+
|
23
|
+
if !@allow.include? :inactive_end_state
|
24
|
+
valid = last_end_nil?(sorted_history, record.errors)
|
25
|
+
return unless valid
|
26
|
+
end
|
27
|
+
|
28
|
+
no_self_trans = no_gaps = no_overlaps = no_nils = true
|
29
|
+
sorted_history.each_cons(2) do |a|
|
30
|
+
this_entry = a[0]
|
31
|
+
next_entry = a[1]
|
32
|
+
|
33
|
+
if !@allow.include? :self_transitions
|
34
|
+
no_self_trans = no_self_transitions?(this_entry, next_entry, record.errors)
|
35
|
+
end
|
36
|
+
|
37
|
+
if !@allow.include? :gaps
|
38
|
+
no_gaps = no_gaps?(this_entry, next_entry, record.errors)
|
39
|
+
end
|
40
|
+
|
41
|
+
if !@allow.include? :overlaps
|
42
|
+
no_overlaps = no_overlaps?(this_entry, next_entry, record.errors)
|
43
|
+
end
|
44
|
+
|
45
|
+
if !@allow.include? :active_middle_states
|
46
|
+
no_nils = no_intervening_nils?(this_entry, record.errors)
|
47
|
+
end
|
48
|
+
|
49
|
+
return unless no_self_trans && no_gaps && no_overlaps && no_nils
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def last_end_nil?(history, errors)
|
56
|
+
last = history.last
|
57
|
+
|
58
|
+
if !last[@end].nil?
|
59
|
+
errors[:base] << "no nil ending"
|
60
|
+
return false
|
61
|
+
end
|
62
|
+
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
def no_self_transitions?(this_entry, next_entry, errors)
|
67
|
+
if matches?(this_entry, next_entry) && this_entry[@end] == next_entry[@start]
|
68
|
+
errors[:base] << "self-transition found"
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def no_gaps?(this_entry, next_entry, errors)
|
76
|
+
if !this_entry[@end].nil? && next_entry[@start] > this_entry[@end]
|
77
|
+
errors[:base] << "Gap found"
|
78
|
+
return false
|
79
|
+
end
|
80
|
+
|
81
|
+
true
|
82
|
+
end
|
83
|
+
|
84
|
+
def no_overlaps?(this_entry, next_entry, errors)
|
85
|
+
if !this_entry[@end].nil? && next_entry[@start] < this_entry[@end]
|
86
|
+
errors[:base] << "Overlap found"
|
87
|
+
return false
|
88
|
+
end
|
89
|
+
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
# This doesn't get invoked on the very last entry, so we should be safe
|
94
|
+
def no_intervening_nils?(this_entry, errors)
|
95
|
+
if this_entry[@end].blank?
|
96
|
+
errors[:base] << "Found nil end in middle of state history"
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
# Match two entries based on the specified attributes in @match
|
104
|
+
def matches?(entry1, entry2)
|
105
|
+
return false if @match.size == 0
|
106
|
+
|
107
|
+
@match.each do |attr|
|
108
|
+
return false if entry1[attr] != entry2[attr]
|
109
|
+
end
|
110
|
+
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Note to self: the module file in lib/ needs to have the same name as this file
|
4
|
+
$:.push File.expand_path("../lib", __FILE__)
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.authors = ["Daniel Ge", "PagerDuty, Inc."]
|
8
|
+
gem.email = ["daniel@pagerduty.com"]
|
9
|
+
gem.summary = "State History and State History Entry Validators"
|
10
|
+
gem.description = "A gem that verifies the integrity of a state history"
|
11
|
+
gem.homepage = "https://github.com/PagerDuty/state-history-validator"
|
12
|
+
|
13
|
+
gem.files = `git ls-files`.split($\)
|
14
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
16
|
+
gem.name = "state_validations"
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
gem.version = "0.2.0"
|
19
|
+
|
20
|
+
# Dependencies
|
21
|
+
gem.add_runtime_dependency "activemodel", ">= 3.0.0"
|
22
|
+
gem.add_runtime_dependency "activesupport", ">= 3.0.0"
|
23
|
+
|
24
|
+
gem.add_development_dependency "rake"
|
25
|
+
gem.add_development_dependency "test-unit", "= 2.2"
|
26
|
+
gem.add_development_dependency "mocha"
|
27
|
+
gem.add_development_dependency "shoulda"
|
28
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class HelperMethodsTest < ActiveSupport::TestCase
|
4
|
+
context "The helper methods on a perfectly valid state history" do
|
5
|
+
setup do
|
6
|
+
now = DateTime.new(2011, 2, 3, 5, 8, 13)
|
7
|
+
@starts = [now, now + 2.months, now + 3.months, now + 5.months]
|
8
|
+
@ends = [now + 2.months, now + 3.months, now + 5.months, nil]
|
9
|
+
end
|
10
|
+
|
11
|
+
should "simply work" do
|
12
|
+
record_entries = []
|
13
|
+
@starts.each_with_index do |start, i|
|
14
|
+
_end = @ends[i]
|
15
|
+
id = i
|
16
|
+
entry = RecordEntry.new(start, _end, id)
|
17
|
+
|
18
|
+
assert entry.valid?
|
19
|
+
|
20
|
+
record_entries << entry
|
21
|
+
end
|
22
|
+
|
23
|
+
record_entries.stubs(:order).with("start ASC, ISNULL(end) ASC, end ASC").returns(record_entries)
|
24
|
+
record_entries.stubs(:all).returns(record_entries)
|
25
|
+
|
26
|
+
record = Record.new(record_entries)
|
27
|
+
|
28
|
+
assert record.valid?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
class Record < Struct.new(:record_entries)
|
35
|
+
include ActiveModel::Validations
|
36
|
+
|
37
|
+
validate_state_history :record_entries,
|
38
|
+
:self_transition_match => [:id]
|
39
|
+
end
|
40
|
+
|
41
|
+
class RecordEntry < Struct.new(:start, :end, :id)
|
42
|
+
include ActiveModel::Validations
|
43
|
+
|
44
|
+
validate_state_history_entry
|
45
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module Validations
|
5
|
+
|
6
|
+
class StateHistoryEntryValidatorTest < ActiveSupport::TestCase
|
7
|
+
context "An \"perfect\" account state history entry" do
|
8
|
+
setup do
|
9
|
+
@now = DateTime.new(2011, 2, 3, 5, 8, 13)
|
10
|
+
end
|
11
|
+
|
12
|
+
should "be valid by default with no checks disabled" do
|
13
|
+
entry = ValidatableEntry.new(@now, @now + 1.days)
|
14
|
+
|
15
|
+
validator = StateHistoryEntryValidator.new
|
16
|
+
validator.validate(entry)
|
17
|
+
|
18
|
+
assert !entry.errors.any?
|
19
|
+
end
|
20
|
+
|
21
|
+
should "be valid if :start and :end are changed" do
|
22
|
+
MockEntry = Struct.new(:begin, :finish)
|
23
|
+
MockEntry.class_eval do
|
24
|
+
include ActiveModel::Validations
|
25
|
+
end
|
26
|
+
entry = MockEntry.new(@now, @now + 1.days)
|
27
|
+
|
28
|
+
validator = StateHistoryEntryValidator.new(
|
29
|
+
:start => :begin,
|
30
|
+
:end => :finish
|
31
|
+
)
|
32
|
+
validator.validate(entry)
|
33
|
+
|
34
|
+
assert !entry.errors.any?
|
35
|
+
end
|
36
|
+
|
37
|
+
should "be valid even if the end is nil" do
|
38
|
+
entry = ValidatableEntry.new(@now, nil)
|
39
|
+
|
40
|
+
validator = StateHistoryEntryValidator.new
|
41
|
+
validator.validate(entry)
|
42
|
+
|
43
|
+
assert !entry.errors.any?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "An account state history entry where start == end" do
|
48
|
+
setup do
|
49
|
+
@now = DateTime.new(2011, 2, 3, 5, 8, 13)
|
50
|
+
@entry = ValidatableEntry.new(@now, @now)
|
51
|
+
end
|
52
|
+
|
53
|
+
should "not be valid with no checks disabled" do
|
54
|
+
validator = StateHistoryEntryValidator.new
|
55
|
+
validator.validate(@entry)
|
56
|
+
|
57
|
+
assert @entry.errors.any?
|
58
|
+
end
|
59
|
+
|
60
|
+
should "be valid with :zero_duration allowed" do
|
61
|
+
validator = StateHistoryEntryValidator.new(
|
62
|
+
:allow => [:zero_duration]
|
63
|
+
)
|
64
|
+
validator.validate(@entry)
|
65
|
+
|
66
|
+
assert !@entry.errors.any?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context "An account state history entry where start > end" do
|
71
|
+
setup do
|
72
|
+
@now = DateTime.new(2011, 2, 3, 5, 8, 13)
|
73
|
+
@entry = ValidatableEntry.new(@now + 1.days, @now)
|
74
|
+
end
|
75
|
+
|
76
|
+
should "not be valid with no checks disabled" do
|
77
|
+
validator = StateHistoryEntryValidator.new
|
78
|
+
validator.validate(@entry)
|
79
|
+
|
80
|
+
assert @entry.errors.any?
|
81
|
+
end
|
82
|
+
|
83
|
+
should "be valid with :end_before_start allowed" do
|
84
|
+
validator = StateHistoryEntryValidator.new(
|
85
|
+
:allow => [:end_before_start]
|
86
|
+
)
|
87
|
+
validator.validate(@entry)
|
88
|
+
|
89
|
+
assert !@entry.errors.any?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
class ValidatableEntry < Struct.new(:start, :end)
|
96
|
+
include ActiveModel::Validations
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,308 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module Validations
|
5
|
+
|
6
|
+
class StateHistoryValidatorTest < ActiveSupport::TestCase
|
7
|
+
context "A perfectly valid state history" do
|
8
|
+
setup do
|
9
|
+
now = DateTime.new(2011, 2, 3, 5, 8, 13)
|
10
|
+
@starts = [now, now + 2.months, now + 3.months, now + 5.months]
|
11
|
+
@ends = [now + 2.months, now + 3.months, now + 5.months, nil]
|
12
|
+
end
|
13
|
+
|
14
|
+
should "simply be valid with the default options" do
|
15
|
+
state_history = mock_state_history(@starts, @ends) do |entry, i|
|
16
|
+
entry[:other_id] = i + 1
|
17
|
+
end
|
18
|
+
|
19
|
+
record = Validatable.new(state_history)
|
20
|
+
validator = StateHistoryValidator.new(
|
21
|
+
:association => :state_history_entries,
|
22
|
+
:self_transition_match => [:id, :other_id]
|
23
|
+
)
|
24
|
+
validator.validate(record)
|
25
|
+
|
26
|
+
assert !record.errors.any?
|
27
|
+
end
|
28
|
+
|
29
|
+
should "be valid if we only set a different start and end name" do
|
30
|
+
MockEntry = Struct.new(:begin, :finish, :id, :other_id)
|
31
|
+
state_history = mock_state_history(@starts, @ends, nil,
|
32
|
+
{:entry_class => MockEntry,
|
33
|
+
:start => :begin,
|
34
|
+
:end => :finish}) do |entry, i|
|
35
|
+
entry[:other_id] = i + 1
|
36
|
+
end
|
37
|
+
|
38
|
+
record = Validatable.new(state_history)
|
39
|
+
validator = StateHistoryValidator.new(
|
40
|
+
:association => :state_history_entries,
|
41
|
+
:start => :begin,
|
42
|
+
:end => :finish,
|
43
|
+
:self_transition_match => [:id, :other_id]
|
44
|
+
)
|
45
|
+
validator.validate(record)
|
46
|
+
|
47
|
+
assert !record.errors.any?
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
context "A \"perfect\" state history with a self-transition" do
|
53
|
+
setup do
|
54
|
+
now = DateTime.new(2011, 2, 3, 5, 8, 13)
|
55
|
+
starts = [now, now + 2.months, now + 3.months, now + 5.months]
|
56
|
+
ends = [now + 2.months, now + 3.months, now + 5.months, nil]
|
57
|
+
ids = [0, 1, 1, 2]
|
58
|
+
other_id = [1, 1, 1, 2]
|
59
|
+
|
60
|
+
state_history = mock_state_history(starts, ends, ids) do |entry, i|
|
61
|
+
entry[:other_id] = other_id[i]
|
62
|
+
end
|
63
|
+
|
64
|
+
@record = Validatable.new(state_history)
|
65
|
+
end
|
66
|
+
|
67
|
+
should "be invalid with nothing allowed" do
|
68
|
+
validator = StateHistoryValidator.new(
|
69
|
+
:association => :state_history_entries,
|
70
|
+
:self_transition_match => [:id, :other_id]
|
71
|
+
)
|
72
|
+
validator.validate(@record)
|
73
|
+
|
74
|
+
assert @record.errors.any?
|
75
|
+
end
|
76
|
+
|
77
|
+
should "be valid with :self_transitions allowed" do
|
78
|
+
validator = StateHistoryValidator.new(
|
79
|
+
:association => :state_history_entries,
|
80
|
+
:self_transition_match => [:id, :other_id],
|
81
|
+
:allow => [:self_transitions]
|
82
|
+
)
|
83
|
+
validator.validate(@record)
|
84
|
+
|
85
|
+
assert !@record.errors.any?
|
86
|
+
end
|
87
|
+
|
88
|
+
should "be valid if no :self_transition_match is specified and nothing is allowed" do
|
89
|
+
validator = StateHistoryValidator.new(
|
90
|
+
:association => :state_history_entries
|
91
|
+
)
|
92
|
+
validator.validate(@record)
|
93
|
+
|
94
|
+
assert !@record.errors.any?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "A \"perfect\" state history with a non-nil end" do
|
99
|
+
setup do
|
100
|
+
now = DateTime.new(2011, 2, 3, 5, 8, 13)
|
101
|
+
starts = [now, now + 2.months, now + 3.months, now + 5.months]
|
102
|
+
ends = [now + 2.months, now + 3.months, now + 5.months, now + 8.months]
|
103
|
+
|
104
|
+
@state_history = mock_state_history(starts, ends, nil) {}
|
105
|
+
|
106
|
+
@record = Validatable.new(@state_history)
|
107
|
+
end
|
108
|
+
|
109
|
+
should "not be valid with nothing allowed" do
|
110
|
+
validator = StateHistoryValidator.new(
|
111
|
+
:association => :state_history_entries,
|
112
|
+
:self_transition_match => [:id]
|
113
|
+
)
|
114
|
+
validator.validate(@record)
|
115
|
+
|
116
|
+
assert @record.errors.any?
|
117
|
+
end
|
118
|
+
|
119
|
+
should "be valid with :inactive_end_state allowed" do
|
120
|
+
validator = StateHistoryValidator.new(
|
121
|
+
:association => :state_history_entries,
|
122
|
+
:self_transition_match => [:id],
|
123
|
+
:allow => [:inactive_end_state]
|
124
|
+
)
|
125
|
+
validator.validate(@record)
|
126
|
+
|
127
|
+
assert !@record.errors.any?
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context "A state history with overlaps" do
|
132
|
+
setup do
|
133
|
+
now = DateTime.new(2011, 2, 3, 5, 8, 13)
|
134
|
+
starts = [now, now + 2.months, now + 3.months, now + 5.months]
|
135
|
+
ends = [now + 2.months, now + 3.months, now + 6.months, nil]
|
136
|
+
|
137
|
+
@state_history = mock_state_history(starts, ends) {}
|
138
|
+
|
139
|
+
@record = Validatable.new(@state_history)
|
140
|
+
end
|
141
|
+
|
142
|
+
should "not be valid with nothing allowed" do
|
143
|
+
validator = StateHistoryValidator.new(
|
144
|
+
:association => :state_history_entries,
|
145
|
+
:self_transition_match => [:id]
|
146
|
+
)
|
147
|
+
validator.validate(@record)
|
148
|
+
|
149
|
+
assert @record.errors.any?
|
150
|
+
end
|
151
|
+
|
152
|
+
should "be valid with :overlaps allowed" do
|
153
|
+
validator = StateHistoryValidator.new(
|
154
|
+
:association => :state_history_entries,
|
155
|
+
:self_transition_match => [:id],
|
156
|
+
:allow => [:overlaps]
|
157
|
+
)
|
158
|
+
validator.validate(@record)
|
159
|
+
|
160
|
+
assert !@record.errors.any?
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
context "A state history with gaps" do
|
165
|
+
setup do
|
166
|
+
now = DateTime.new(2011, 2, 3, 5, 8, 13)
|
167
|
+
starts = [now, now + 2.months, now + 3.months, now + 5.months]
|
168
|
+
ends = [now + 2.months, now + 3.months, now + 4.months, nil]
|
169
|
+
|
170
|
+
state_history = mock_state_history(starts, ends) {}
|
171
|
+
@record = Validatable.new(state_history)
|
172
|
+
end
|
173
|
+
|
174
|
+
should "not be valid with nothing allowed" do
|
175
|
+
validator = StateHistoryValidator.new(
|
176
|
+
:association => :state_history_entries,
|
177
|
+
:self_transition_match => [:id]
|
178
|
+
)
|
179
|
+
validator.validate(@record)
|
180
|
+
|
181
|
+
assert @record.errors.any?
|
182
|
+
end
|
183
|
+
|
184
|
+
should "be valid with :gaps allowed" do
|
185
|
+
validator = StateHistoryValidator.new(
|
186
|
+
:association => :state_history_entries,
|
187
|
+
:self_transition_match => [:id],
|
188
|
+
:allow => [:gaps]
|
189
|
+
)
|
190
|
+
validator.validate(@record)
|
191
|
+
|
192
|
+
assert !@record.errors.any?
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
context "A state history with nil `:ends` in the middle" do
|
197
|
+
setup do
|
198
|
+
now = DateTime.new(2011, 2, 3, 5, 8, 13)
|
199
|
+
@starts = [now, now + 2.months, now + 3.months, now + 5.months]
|
200
|
+
@ends = [now + 2.months, nil, now + 5.months, nil]
|
201
|
+
|
202
|
+
state_history = mock_state_history(@starts, @ends) {}
|
203
|
+
@record = Validatable.new(state_history)
|
204
|
+
end
|
205
|
+
|
206
|
+
should "not be valid with nothing allowed" do
|
207
|
+
validator = StateHistoryValidator.new(
|
208
|
+
:association => :state_history_entries,
|
209
|
+
:self_transition_match => [:id]
|
210
|
+
)
|
211
|
+
validator.validate(@record)
|
212
|
+
|
213
|
+
assert @record.errors.any?
|
214
|
+
end
|
215
|
+
|
216
|
+
should "be valid with :active_middle_states allowed" do
|
217
|
+
validator = StateHistoryValidator.new(
|
218
|
+
:association => :state_history_entries,
|
219
|
+
:self_transition_match => [:id],
|
220
|
+
:allow => [:active_middle_states]
|
221
|
+
)
|
222
|
+
validator.validate(@record)
|
223
|
+
|
224
|
+
assert !@record.errors.any?
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
context "A state history with two consecutive equivalent states separated by a gap" do
|
229
|
+
setup do
|
230
|
+
now = DateTime.new(2011, 2, 3, 5, 8, 13)
|
231
|
+
@starts = [now, now + 2.months]
|
232
|
+
@ends = [now + 1.months, nil]
|
233
|
+
@id = [0, 0]
|
234
|
+
|
235
|
+
state_history = mock_state_history(@starts, @ends, @id) {}
|
236
|
+
|
237
|
+
@record = Validatable.new(state_history)
|
238
|
+
end
|
239
|
+
|
240
|
+
should "not be valid with nothing allowed" do
|
241
|
+
validator = StateHistoryValidator.new(
|
242
|
+
:association => :state_history_entries,
|
243
|
+
:self_transition_match => [:id]
|
244
|
+
)
|
245
|
+
validator.validate(@record)
|
246
|
+
|
247
|
+
assert @record.errors.any?
|
248
|
+
end
|
249
|
+
|
250
|
+
should "be valid with :gaps allowed" do
|
251
|
+
validator = StateHistoryValidator.new(
|
252
|
+
:association => :state_history_entries,
|
253
|
+
:self_transition_match => [:id],
|
254
|
+
:allow => [:gaps]
|
255
|
+
)
|
256
|
+
validator.validate(@record)
|
257
|
+
|
258
|
+
assert !@record.errors.any?
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
private
|
263
|
+
|
264
|
+
class Validatable < Struct.new(:state_history_entries)
|
265
|
+
include ActiveModel::Validations
|
266
|
+
end
|
267
|
+
|
268
|
+
class ValidatableEntry < Struct.new(:start, :end, :id, :other_id)
|
269
|
+
end
|
270
|
+
|
271
|
+
# fake the entries.order(@order).all call
|
272
|
+
def state_history_stub(state_history, options = {})
|
273
|
+
start_attr = options[:start] || :start
|
274
|
+
end_attr = options[:end] || :end
|
275
|
+
sort_dir = options[:sort_dir] || "ASC"
|
276
|
+
|
277
|
+
order_by = options[:order] ||
|
278
|
+
"#{start_attr} #{sort_dir}, ISNULL(#{end_attr}) #{sort_dir}, #{end_attr} #{sort_dir}"
|
279
|
+
|
280
|
+
state_history.stubs(:order).with(order_by).returns(state_history)
|
281
|
+
state_history.stubs(:all).returns(state_history)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Set up a state_history with fake objects and then yield to allow further customization
|
285
|
+
def mock_state_history(starts, ends, ids = nil, options = {})
|
286
|
+
stub_history = (options[:stub_history].nil?) ? true : options[:stub_history]
|
287
|
+
entry_class = (options[:entry_class].nil?) ? ValidatableEntry : options[:entry_class]
|
288
|
+
|
289
|
+
state_history = []
|
290
|
+
starts.each_with_index do |start, i|
|
291
|
+
_end = ends[i]
|
292
|
+
id = (ids.nil?) ? i : ids[i]
|
293
|
+
entry = entry_class.new(start, _end, id)
|
294
|
+
|
295
|
+
yield entry, i
|
296
|
+
|
297
|
+
state_history << entry
|
298
|
+
end
|
299
|
+
|
300
|
+
state_history_stub(state_history, options) if stub_history
|
301
|
+
|
302
|
+
state_history
|
303
|
+
end
|
304
|
+
|
305
|
+
end
|
306
|
+
|
307
|
+
end
|
308
|
+
end
|
metadata
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: state_validations
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Daniel Ge
|
14
|
+
- PagerDuty, Inc.
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2012-07-18 00:00:00 Z
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
hash: 7
|
28
|
+
segments:
|
29
|
+
- 3
|
30
|
+
- 0
|
31
|
+
- 0
|
32
|
+
version: 3.0.0
|
33
|
+
prerelease: false
|
34
|
+
type: :runtime
|
35
|
+
name: activemodel
|
36
|
+
requirement: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 7
|
44
|
+
segments:
|
45
|
+
- 3
|
46
|
+
- 0
|
47
|
+
- 0
|
48
|
+
version: 3.0.0
|
49
|
+
prerelease: false
|
50
|
+
type: :runtime
|
51
|
+
name: activesupport
|
52
|
+
requirement: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
prerelease: false
|
64
|
+
type: :development
|
65
|
+
name: rake
|
66
|
+
requirement: *id003
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - "="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 7
|
74
|
+
segments:
|
75
|
+
- 2
|
76
|
+
- 2
|
77
|
+
version: "2.2"
|
78
|
+
prerelease: false
|
79
|
+
type: :development
|
80
|
+
name: test-unit
|
81
|
+
requirement: *id004
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
hash: 3
|
89
|
+
segments:
|
90
|
+
- 0
|
91
|
+
version: "0"
|
92
|
+
prerelease: false
|
93
|
+
type: :development
|
94
|
+
name: mocha
|
95
|
+
requirement: *id005
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
version_requirements: &id006 !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
hash: 3
|
103
|
+
segments:
|
104
|
+
- 0
|
105
|
+
version: "0"
|
106
|
+
prerelease: false
|
107
|
+
type: :development
|
108
|
+
name: shoulda
|
109
|
+
requirement: *id006
|
110
|
+
description: A gem that verifies the integrity of a state history
|
111
|
+
email:
|
112
|
+
- daniel@pagerduty.com
|
113
|
+
executables: []
|
114
|
+
|
115
|
+
extensions: []
|
116
|
+
|
117
|
+
extra_rdoc_files: []
|
118
|
+
|
119
|
+
files:
|
120
|
+
- .gitignore
|
121
|
+
- Gemfile
|
122
|
+
- LICENSE
|
123
|
+
- README.md
|
124
|
+
- Rakefile
|
125
|
+
- lib/active_model/validations/helper_methods.rb
|
126
|
+
- lib/active_model/validations/state_history_entry_validator.rb
|
127
|
+
- lib/active_model/validations/state_history_validator.rb
|
128
|
+
- lib/state_validations.rb
|
129
|
+
- state_validations.gemspec
|
130
|
+
- test/test_helper.rb
|
131
|
+
- test/unit/helper_methods_test.rb
|
132
|
+
- test/unit/state_history_entry_validator_test.rb
|
133
|
+
- test/unit/state_history_validator_test.rb
|
134
|
+
homepage: https://github.com/PagerDuty/state-history-validator
|
135
|
+
licenses: []
|
136
|
+
|
137
|
+
post_install_message:
|
138
|
+
rdoc_options: []
|
139
|
+
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
none: false
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
hash: 3
|
148
|
+
segments:
|
149
|
+
- 0
|
150
|
+
version: "0"
|
151
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
152
|
+
none: false
|
153
|
+
requirements:
|
154
|
+
- - ">="
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
hash: 3
|
157
|
+
segments:
|
158
|
+
- 0
|
159
|
+
version: "0"
|
160
|
+
requirements: []
|
161
|
+
|
162
|
+
rubyforge_project:
|
163
|
+
rubygems_version: 1.8.24
|
164
|
+
signing_key:
|
165
|
+
specification_version: 3
|
166
|
+
summary: State History and State History Entry Validators
|
167
|
+
test_files:
|
168
|
+
- test/test_helper.rb
|
169
|
+
- test/unit/helper_methods_test.rb
|
170
|
+
- test/unit/state_history_entry_validator_test.rb
|
171
|
+
- test/unit/state_history_validator_test.rb
|